From d66ddb09ca166419bbac29c66cf4fa4c4c71bb0e Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:57:15 -0600 Subject: [PATCH 01/25] Add PipelineOptions and StructuredPipeline --- packages/firestore/src/api/pipeline_impl.ts | 96 +++-- .../firestore/src/core/firestore_client.ts | 3 +- .../firestore/src/core/structured_pipeline.ts | 81 +++++ .../src/lite-api/pipeline_settings.ts | 61 ++++ packages/firestore/src/remote/datastore.ts | 8 +- .../unit/core/structured_pipeline.test.ts | 332 ++++++++++++++++++ 6 files changed, 549 insertions(+), 32 deletions(-) create mode 100644 packages/firestore/src/core/structured_pipeline.ts create mode 100644 packages/firestore/src/lite-api/pipeline_settings.ts create mode 100644 packages/firestore/test/unit/core/structured_pipeline.test.ts diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts index ba6e08105bb..460e4eb9536 100644 --- a/packages/firestore/src/api/pipeline_impl.ts +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -21,12 +21,23 @@ import { Pipeline as LitePipeline } from '../lite-api/pipeline'; import { PipelineResult, PipelineSnapshot } from '../lite-api/pipeline-result'; import { PipelineSource } from '../lite-api/pipeline-source'; import { Stage } from '../lite-api/stage'; -import { newUserDataReader } from '../lite-api/user_data_reader'; +import { + newUserDataReader, + parseData, + UserDataReader, + UserDataSource +} from '../lite-api/user_data_reader'; import { cast } from '../util/input_validation'; import { ensureFirestoreConfigured, Firestore } from './database'; import { DocumentReference } from './reference'; import { ExpUserDataWriter } from './user_data_writer'; +import { PipelineOptions } from '../lite-api/pipeline_settings'; +import { + StructuredPipeline, + StructuredPipelineOptions +} from '../core/structured_pipeline'; +import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; declare module './database' { interface Firestore { @@ -68,35 +79,68 @@ declare module './database' { * @param pipeline The pipeline to execute. * @return A Promise representing the asynchronous pipeline execution. */ -export function execute(pipeline: LitePipeline): Promise { +export function execute(pipeline: LitePipeline): Promise; +export function execute(options: PipelineOptions): Promise; +export function execute( + pipelineOrOptions: LitePipeline | PipelineOptions +): Promise { + let pipeline: LitePipeline = + pipelineOrOptions instanceof LitePipeline + ? pipelineOrOptions + : pipelineOrOptions.pipeline; + let options: StructuredPipelineOptions = !( + pipelineOrOptions instanceof LitePipeline + ) + ? pipelineOrOptions + : {}; + let genericOptions: { [name: string]: unknown } = + (pipelineOrOptions as PipelineOptions).genericOptions ?? {}; + const firestore = cast(pipeline._db, Firestore); const client = ensureFirestoreConfigured(firestore); - return firestoreClientExecutePipeline(client, pipeline).then(result => { - // Get the execution time from the first result. - // firestoreClientExecutePipeline returns at least one PipelineStreamElement - // even if the returned document set is empty. - const executionTime = - result.length > 0 ? result[0].executionTime?.toTimestamp() : undefined; - const docs = result - // Currently ignore any response from ExecutePipeline that does - // not contain any document data in the `fields` property. - .filter(element => !!element.fields) - .map( - element => - new PipelineResult( - pipeline._userDataWriter, - element.key?.path - ? new DocumentReference(firestore, null, element.key) - : undefined, - element.fields, - element.createTime?.toTimestamp(), - element.updateTime?.toTimestamp() - ) - ); + const udr = new UserDataReader( + firestore._databaseId, + /* ignoreUndefinedProperties */ true + ); + const context = udr.createContext(UserDataSource.Argument, 'execute'); + const optionsOverride: ApiClientObjectMap = + parseData(genericOptions, context)?.mapValue?.fields ?? {}; - return new PipelineSnapshot(pipeline, docs, executionTime); - }); + let structuredPipeline: StructuredPipeline = new StructuredPipeline( + pipeline, + options, + optionsOverride + ); + + return firestoreClientExecutePipeline(client, structuredPipeline).then( + result => { + // Get the execution time from the first result. + // firestoreClientExecutePipeline returns at least one PipelineStreamElement + // even if the returned document set is empty. + const executionTime = + result.length > 0 ? result[0].executionTime?.toTimestamp() : undefined; + + const docs = result + // Currently ignore any response from ExecutePipeline that does + // not contain any document data in the `fields` property. + .filter(element => !!element.fields) + .map( + element => + new PipelineResult( + pipeline._userDataWriter, + element.key?.path + ? new DocumentReference(firestore, null, element.key) + : undefined, + element.fields, + element.createTime?.toTimestamp(), + element.updateTime?.toTimestamp() + ) + ); + + return new PipelineSnapshot(pipeline, docs, executionTime); + } + ); } // Augment the Firestore class with the pipeline() factory method diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index bb0771d2335..bb1c19931d7 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -102,6 +102,7 @@ import { TransactionOptions } from './transaction_options'; import { TransactionRunner } from './transaction_runner'; import { View } from './view'; import { ViewSnapshot } from './view_snapshot'; +import { StructuredPipeline } from './structured_pipeline'; const LOG_TAG = 'FirestoreClient'; export const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100; @@ -557,7 +558,7 @@ export function firestoreClientRunAggregateQuery( export function firestoreClientExecutePipeline( client: FirestoreClient, - pipeline: Pipeline + pipeline: StructuredPipeline ): Promise { const deferred = new Deferred(); diff --git a/packages/firestore/src/core/structured_pipeline.ts b/packages/firestore/src/core/structured_pipeline.ts new file mode 100644 index 00000000000..b748cf10805 --- /dev/null +++ b/packages/firestore/src/core/structured_pipeline.ts @@ -0,0 +1,81 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + StructuredPipeline as StructuredPipelineProto, + Pipeline as PipelineProto, + ApiClientObjectMap, + Value +} from '../protos/firestore_proto_api'; + +import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; +import { ObjectValue } from '../model/object_value'; +import { FieldPath } from '../model/path'; +import { mapToArray } from '../util/obj'; + +export interface StructuredPipelineOptions { + indexMode?: 'recommended'; +} + +export class StructuredPipeline + implements ProtoSerializable +{ + constructor( + private pipeline: ProtoSerializable, + private options: StructuredPipelineOptions, + private optionsOverride: ApiClientObjectMap + ) {} + + /** + * @private + * @internal for testing + */ + _getKnownOptions(): ObjectValue { + const options: ObjectValue = ObjectValue.empty(); + + /** SERIALIZE KNOWN OPTIONS **/ + if (typeof this.options.indexMode === 'string') { + options.set(FieldPath.fromServerFormat('index_mode'), { + stringValue: this.options.indexMode + }); + } + + return options; + } + + private getOptionsProto(): ApiClientObjectMap { + const options: ObjectValue = this._getKnownOptions(); + + /** APPLY OPTIONS OVERRIDES **/ + const optionsMap = new Map( + mapToArray(this.optionsOverride, (value, key) => [ + FieldPath.fromServerFormat(key), + value + ]) + ); + options.setAll(optionsMap); + + return options.value.mapValue.fields ?? {}; + } + + _toProto(serializer: JsonProtoSerializer): StructuredPipelineProto { + return { + pipeline: this.pipeline._toProto(serializer), + options: this.getOptionsProto() + }; + } +} diff --git a/packages/firestore/src/lite-api/pipeline_settings.ts b/packages/firestore/src/lite-api/pipeline_settings.ts new file mode 100644 index 00000000000..e23cc33b69a --- /dev/null +++ b/packages/firestore/src/lite-api/pipeline_settings.ts @@ -0,0 +1,61 @@ +import type { Pipeline } from './pipeline'; + +/** + * Options defining how a Pipeline is evaluated. + */ +export interface PipelineOptions { + /** + * Pipeline to be evaluated. + */ + pipeline: Pipeline; + + /** + * Specify the index mode. + */ + indexMode?: 'recommended'; + + /** + * An escape hatch to set options not known at SDK build time. These values + * will be passed directly to the Firestore backend and not used by the SDK. + * + * The generic option name will be used as provided. And must match the name + * format used by the backend (hint: use a snake_case_name). + * + * Generic option values can be any type supported + * by Firestore (for example: string, boolean, number, map, …). Value types + * not known to the SDK will be rejected. + * + * Values specified in genericOptions will take precedence over any options + * with the same name set by the SDK. + * + * Override the `example_option`: + * ``` + * execute({ + * pipeline: myPipeline, + * genericOptions: { + * // Override `example_option`. This will not + * // merge with the existing `example_option` object. + * "example_option": { + * foo: "bar" + * } + * } + * } + * ``` + * + * `genericOptions` supports dot notation, if you want to override + * a nested option. + * ``` + * execute({ + * pipeline: myPipeline, + * genericOptions: { + * // Override `example_option.foo` and do not override + * // any other properties of `example_option`. + * "example_option.foo": "bar" + * } + * } + * ``` + */ + genericOptions?: { + [name: string]: unknown; + }; +} diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index 32666feeea1..793907cc420 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -20,7 +20,7 @@ import { User } from '../auth/user'; import { Aggregate } from '../core/aggregate'; import { DatabaseId } from '../core/database_info'; import { queryToAggregateTarget, Query, queryToTarget } from '../core/query'; -import { Pipeline } from '../lite-api/pipeline'; +import { StructuredPipeline } from '../core/structured_pipeline'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; @@ -242,14 +242,12 @@ export async function invokeBatchGetDocumentsRpc( export async function invokeExecutePipeline( datastore: Datastore, - pipeline: Pipeline + structuredPipeline: StructuredPipeline ): Promise { const datastoreImpl = debugCast(datastore, DatastoreImpl); const executePipelineRequest: ProtoExecutePipelineRequest = { database: getEncodedDatabaseId(datastoreImpl.serializer), - structuredPipeline: { - pipeline: pipeline._toProto(datastoreImpl.serializer) - } + structuredPipeline: structuredPipeline._toProto(datastoreImpl.serializer) }; const response = await datastoreImpl.invokeStreamingRPC< diff --git a/packages/firestore/test/unit/core/structured_pipeline.test.ts b/packages/firestore/test/unit/core/structured_pipeline.test.ts new file mode 100644 index 00000000000..b924c6341ee --- /dev/null +++ b/packages/firestore/test/unit/core/structured_pipeline.test.ts @@ -0,0 +1,332 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { StructuredPipeline } from '../../../src/core/structured_pipeline'; +import { + JsonProtoSerializer, + ProtoSerializable +} from '../../../src/remote/serializer'; + +import { Pipeline as PipelineProto } from '../../../src/protos/firestore_proto_api'; +import { DatabaseId } from '../../../src/core/database_info'; +import { ObjectValue } from '../../../src/model/object_value'; + +describe('StructuredPipeline', () => { + it('should serialize the pipeline argument', () => { + let pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const structuredPipeline = new StructuredPipeline(pipeline, {}, {}); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: {} + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it('should support known options', () => { + let pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const structuredPipeline = new StructuredPipeline( + pipeline, + { + indexMode: 'recommended' + }, + {} + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + index_mode: { + stringValue: 'recommended' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it.only('should support unknown options', () => { + let pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const structuredPipeline = new StructuredPipeline( + pipeline, + {}, + { + 'foo_bar': { stringValue: 'baz' } + } + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo_bar': { + stringValue: 'baz' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it.only('should support unknown nested options', () => { + let pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const structuredPipeline = new StructuredPipeline( + pipeline, + {}, + { + 'foo.bar': { stringValue: 'baz' } + } + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo': { + mapValue: { + fields: { + 'bar': { stringValue: 'baz' } + } + } + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it.only('should support options override', () => { + let pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + const structuredPipeline = new StructuredPipeline( + pipeline, + { + indexMode: 'recommended' + }, + { + 'index_mode': { stringValue: 'baz' } + } + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'index_mode': { + stringValue: 'baz' + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it.only('should support options override of nested field', () => { + let pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + + const structuredPipeline = new StructuredPipeline( + pipeline, + {}, + { + 'foo.bar': { integerValue: 123 } + } + ); + + // Fake known options with a nested {foo: {bar: "baz"}} + structuredPipeline._getKnownOptions = sinon.fake.returns( + new ObjectValue({ + mapValue: { + fields: { + 'foo': { + mapValue: { + fields: { + 'bar': { stringValue: 'baz' }, + 'waldo': { booleanValue: true } + } + } + } + } + } + }) + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo': { + mapValue: { + fields: { + 'bar': { + integerValue: 123 + }, + 'waldo': { + booleanValue: true + } + } + } + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it.only('will replace a nested object if given a new object', () => { + let pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + + const structuredPipeline = new StructuredPipeline( + pipeline, + {}, + { + 'foo': { mapValue: { fields: { bar: { integerValue: 123 } } } } + } + ); + + // Fake known options with a nested {foo: {bar: "baz"}} + structuredPipeline._getKnownOptions = sinon.fake.returns( + new ObjectValue({ + mapValue: { + fields: { + 'foo': { + mapValue: { + fields: { + 'bar': { stringValue: 'baz' }, + 'waldo': { booleanValue: true } + } + } + } + } + } + }) + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo': { + mapValue: { + fields: { + 'bar': { + integerValue: 123 + } + } + } + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); + + it.only('will replace a top level property that is not an object if given a nested field with dot notation', () => { + let pipeline: ProtoSerializable = { + _toProto: sinon.fake.returns({} as PipelineProto) + }; + + const structuredPipeline = new StructuredPipeline( + pipeline, + {}, + { + 'foo': { + mapValue: { + fields: { + 'bar': { stringValue: '123' }, + 'waldo': { booleanValue: true } + } + } + } + } + ); + + // Fake known options with a nested {foo: {bar: "baz"}} + structuredPipeline._getKnownOptions = sinon.fake.returns( + new ObjectValue({ + mapValue: { + fields: { + 'foo': { integerValue: 123 } + } + } + }) + ); + + const proto = structuredPipeline._toProto( + new JsonProtoSerializer(DatabaseId.empty(), false) + ); + + expect(proto).to.deep.equal({ + pipeline: {}, + options: { + 'foo': { + mapValue: { + fields: { + 'bar': { + stringValue: '123' + }, + 'waldo': { + booleanValue: true + } + } + } + } + } + }); + + expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; + }); +}); From 45688b068d1d3a51da2dd54a5c49d6aaa5fcbeb5 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:35:45 -0600 Subject: [PATCH 02/25] Testing for pipeline options --- packages/firestore/src/api/pipeline_impl.ts | 20 +- .../firestore/src/core/firestore_client.ts | 3 +- .../firestore/src/core/structured_pipeline.ts | 9 +- .../test/unit/api/pipeline_impl.test.ts | 199 ++++++++++++++++++ .../unit/core/structured_pipeline.test.ts | 37 ++-- 5 files changed, 232 insertions(+), 36 deletions(-) create mode 100644 packages/firestore/test/unit/api/pipeline_impl.test.ts diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts index 460e4eb9536..c2183ca7e3a 100644 --- a/packages/firestore/src/api/pipeline_impl.ts +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -17,9 +17,14 @@ import { Pipeline } from '../api/pipeline'; import { firestoreClientExecutePipeline } from '../core/firestore_client'; +import { + StructuredPipeline, + StructuredPipelineOptions +} from '../core/structured_pipeline'; import { Pipeline as LitePipeline } from '../lite-api/pipeline'; import { PipelineResult, PipelineSnapshot } from '../lite-api/pipeline-result'; import { PipelineSource } from '../lite-api/pipeline-source'; +import { PipelineOptions } from '../lite-api/pipeline_settings'; import { Stage } from '../lite-api/stage'; import { newUserDataReader, @@ -27,17 +32,12 @@ import { UserDataReader, UserDataSource } from '../lite-api/user_data_reader'; +import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; import { cast } from '../util/input_validation'; import { ensureFirestoreConfigured, Firestore } from './database'; import { DocumentReference } from './reference'; import { ExpUserDataWriter } from './user_data_writer'; -import { PipelineOptions } from '../lite-api/pipeline_settings'; -import { - StructuredPipeline, - StructuredPipelineOptions -} from '../core/structured_pipeline'; -import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; declare module './database' { interface Firestore { @@ -84,16 +84,16 @@ export function execute(options: PipelineOptions): Promise; export function execute( pipelineOrOptions: LitePipeline | PipelineOptions ): Promise { - let pipeline: LitePipeline = + const pipeline: LitePipeline = pipelineOrOptions instanceof LitePipeline ? pipelineOrOptions : pipelineOrOptions.pipeline; - let options: StructuredPipelineOptions = !( + const options: StructuredPipelineOptions = !( pipelineOrOptions instanceof LitePipeline ) ? pipelineOrOptions : {}; - let genericOptions: { [name: string]: unknown } = + const genericOptions: { [name: string]: unknown } = (pipelineOrOptions as PipelineOptions).genericOptions ?? {}; const firestore = cast(pipeline._db, Firestore); @@ -107,7 +107,7 @@ export function execute( const optionsOverride: ApiClientObjectMap = parseData(genericOptions, context)?.mapValue?.fields ?? {}; - let structuredPipeline: StructuredPipeline = new StructuredPipeline( + const structuredPipeline: StructuredPipeline = new StructuredPipeline( pipeline, options, optionsOverride diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index bb1c19931d7..0df5a692128 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -23,7 +23,6 @@ import { CredentialsProvider } from '../api/credentials'; import { User } from '../auth/user'; -import { Pipeline } from '../lite-api/pipeline'; import { LocalStore } from '../local/local_store'; import { localStoreConfigureFieldIndexes, @@ -87,6 +86,7 @@ import { removeSnapshotsInSyncListener } from './event_manager'; import { newQueryForPath, Query } from './query'; +import { StructuredPipeline } from './structured_pipeline'; import { SyncEngine } from './sync_engine'; import { syncEngineListen, @@ -102,7 +102,6 @@ import { TransactionOptions } from './transaction_options'; import { TransactionRunner } from './transaction_runner'; import { View } from './view'; import { ViewSnapshot } from './view_snapshot'; -import { StructuredPipeline } from './structured_pipeline'; const LOG_TAG = 'FirestoreClient'; export const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100; diff --git a/packages/firestore/src/core/structured_pipeline.ts b/packages/firestore/src/core/structured_pipeline.ts index b748cf10805..ef913512275 100644 --- a/packages/firestore/src/core/structured_pipeline.ts +++ b/packages/firestore/src/core/structured_pipeline.ts @@ -15,16 +15,15 @@ * limitations under the License. */ +import { ObjectValue } from '../model/object_value'; +import { FieldPath } from '../model/path'; import { StructuredPipeline as StructuredPipelineProto, Pipeline as PipelineProto, ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; - import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; -import { ObjectValue } from '../model/object_value'; -import { FieldPath } from '../model/path'; import { mapToArray } from '../util/obj'; export interface StructuredPipelineOptions { @@ -47,7 +46,7 @@ export class StructuredPipeline _getKnownOptions(): ObjectValue { const options: ObjectValue = ObjectValue.empty(); - /** SERIALIZE KNOWN OPTIONS **/ + // SERIALIZE KNOWN OPTIONS if (typeof this.options.indexMode === 'string') { options.set(FieldPath.fromServerFormat('index_mode'), { stringValue: this.options.indexMode @@ -60,7 +59,7 @@ export class StructuredPipeline private getOptionsProto(): ApiClientObjectMap { const options: ObjectValue = this._getKnownOptions(); - /** APPLY OPTIONS OVERRIDES **/ + // APPLY OPTIONS OVERRIDES const optionsMap = new Map( mapToArray(this.optionsOverride, (value, key) => [ FieldPath.fromServerFormat(key), diff --git a/packages/firestore/test/unit/api/pipeline_impl.test.ts b/packages/firestore/test/unit/api/pipeline_impl.test.ts new file mode 100644 index 00000000000..c04d2cac6d5 --- /dev/null +++ b/packages/firestore/test/unit/api/pipeline_impl.test.ts @@ -0,0 +1,199 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { Timestamp } from '../../../src'; +import { Firestore } from '../../../src/api/database'; +import { execute } from '../../../src/api/pipeline_impl'; +import { + MemoryOfflineComponentProvider, + OnlineComponentProvider +} from '../../../src/core/component_provider'; +import { + ExecutePipelineRequest as ProtoExecutePipelineRequest, + ExecutePipelineResponse as ProtoExecutePipelineResponse +} from '../../../src/protos/firestore_proto_api'; +import { newTestFirestore } from '../../util/api_helpers'; + +const FIRST_CALL = 0; +const EXECUTE_PIPELINE_REQUEST = 3; + +function fakePipelineResponse( + firestore: Firestore, + response?: ProtoExecutePipelineResponse[] +): sinon.SinonSpy { + response = response ?? [ + { + executionTime: Timestamp.now().toDate().toISOString(), + results: [] + } + ]; + const fake = sinon.fake.resolves(response); + + firestore._componentsProvider = { + _offline: { + build: () => new MemoryOfflineComponentProvider() + }, + _online: { + build: () => { + const provider = new OnlineComponentProvider(); + const ogCreateDatastore = provider.createDatastore.bind(provider); + provider.createDatastore = config => { + const datastore = ogCreateDatastore(config); + // @ts-ignore + datastore.invokeStreamingRPC = fake; + return datastore; + }; + return provider; + } + } + }; + + return fake; +} + +describe('execute(Pipeline|PipelineOptions)', () => { + it('returns execution time with empty results', async () => { + const firestore = newTestFirestore(); + + const executeTime = Timestamp.now(); + const spy = fakePipelineResponse(firestore, [ + { + executionTime: executeTime.toDate().toISOString(), + results: [] + } + ]); + + const pipelineSnapshot = await execute( + firestore.pipeline().collection('foo') + ); + + expect(pipelineSnapshot.results.length).to.equal(0); + expect(spy.calledOnce); + + expect(pipelineSnapshot.executionTime.toJSON()).to.deep.equal( + executeTime.toJSON() + ); + }); + + it('serializes the pipeline', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo') + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': {}, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection' + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); + + it('serializes the pipeline options', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo'), + indexMode: 'recommended' + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': { + 'index_mode': { + 'stringValue': 'recommended' + } + }, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection' + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); + + it('serializes the pipeline generic options', async () => { + const firestore = newTestFirestore(); + const spy = fakePipelineResponse(firestore); + + await execute({ + pipeline: firestore.pipeline().collection('foo'), + genericOptions: { + 'foo': 'bar' + } + }); + + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: 'projects/new-project/databases/(default)', + structuredPipeline: { + 'options': { + 'foo': { + 'stringValue': 'bar' + } + }, + 'pipeline': { + 'stages': [ + { + 'args': [ + { + 'referenceValue': '/foo' + } + ], + 'name': 'collection' + } + ] + } + } + }; + expect(spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]).to.deep.equal( + executePipelineRequest + ); + }); +}); diff --git a/packages/firestore/test/unit/core/structured_pipeline.test.ts b/packages/firestore/test/unit/core/structured_pipeline.test.ts index b924c6341ee..920108df710 100644 --- a/packages/firestore/test/unit/core/structured_pipeline.test.ts +++ b/packages/firestore/test/unit/core/structured_pipeline.test.ts @@ -18,19 +18,18 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; +import { DatabaseId } from '../../../src/core/database_info'; import { StructuredPipeline } from '../../../src/core/structured_pipeline'; +import { ObjectValue } from '../../../src/model/object_value'; +import { Pipeline as PipelineProto } from '../../../src/protos/firestore_proto_api'; import { JsonProtoSerializer, ProtoSerializable } from '../../../src/remote/serializer'; -import { Pipeline as PipelineProto } from '../../../src/protos/firestore_proto_api'; -import { DatabaseId } from '../../../src/core/database_info'; -import { ObjectValue } from '../../../src/model/object_value'; - describe('StructuredPipeline', () => { it('should serialize the pipeline argument', () => { - let pipeline: ProtoSerializable = { + const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; const structuredPipeline = new StructuredPipeline(pipeline, {}, {}); @@ -48,7 +47,7 @@ describe('StructuredPipeline', () => { }); it('should support known options', () => { - let pipeline: ProtoSerializable = { + const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; const structuredPipeline = new StructuredPipeline( @@ -66,7 +65,7 @@ describe('StructuredPipeline', () => { expect(proto).to.deep.equal({ pipeline: {}, options: { - index_mode: { + 'index_mode': { stringValue: 'recommended' } } @@ -75,8 +74,8 @@ describe('StructuredPipeline', () => { expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; }); - it.only('should support unknown options', () => { - let pipeline: ProtoSerializable = { + it('should support unknown options', () => { + const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; const structuredPipeline = new StructuredPipeline( @@ -103,8 +102,8 @@ describe('StructuredPipeline', () => { expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; }); - it.only('should support unknown nested options', () => { - let pipeline: ProtoSerializable = { + it('should support unknown nested options', () => { + const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; const structuredPipeline = new StructuredPipeline( @@ -135,8 +134,8 @@ describe('StructuredPipeline', () => { expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; }); - it.only('should support options override', () => { - let pipeline: ProtoSerializable = { + it('should support options override', () => { + const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; const structuredPipeline = new StructuredPipeline( @@ -165,8 +164,8 @@ describe('StructuredPipeline', () => { expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; }); - it.only('should support options override of nested field', () => { - let pipeline: ProtoSerializable = { + it('should support options override of nested field', () => { + const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; @@ -221,8 +220,8 @@ describe('StructuredPipeline', () => { expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; }); - it.only('will replace a nested object if given a new object', () => { - let pipeline: ProtoSerializable = { + it('will replace a nested object if given a new object', () => { + const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; @@ -274,8 +273,8 @@ describe('StructuredPipeline', () => { expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; }); - it.only('will replace a top level property that is not an object if given a nested field with dot notation', () => { - let pipeline: ProtoSerializable = { + it('will replace a top level property that is not an object if given a nested field with dot notation', () => { + const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; From b3d8c75792bac93f04a513606ff3761d441f7b8d Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:23:48 -0600 Subject: [PATCH 03/25] Test for error handling in Piplines --- .../test/integration/api/pipeline.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index fb04f775972..4bfe2539f98 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { FirebaseError } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; @@ -1618,6 +1619,33 @@ apiDescribe('Pipelines', persistence => { }); }); + describe('error handling', () => { + it('error properties are propagated from the firestore backend', async () => { + try { + const myPipeline = firestore + .pipeline() + .collection(randomCol.path) + .genericStage('select', [ + // incorrect parameter type + field('title'), + ]); + + await execute(myPipeline); + + expect.fail('expected pipeline.execute() to throw'); + } catch (e: unknown) { + expect(e instanceof FirebaseError).to.be.true; + const err = e as FirebaseError; + expect(err['code']).to.equal('invalid-argument'); + expect(typeof err['message']).to.equal('string'); + + expect(err['message']).to.match( + /^3 INVALID_ARGUMENT: .*$/ + ); + } + }); + }); + describe('function expressions', () => { it('logical max works', async () => { const snapshot = await execute( From e111ec1886c4f6bccb39151c3f7310e4c1c68cd9 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:12:26 -0600 Subject: [PATCH 04/25] Refactor and test pipeline options implementation --- packages/firestore/src/api/pipeline_impl.ts | 25 +- .../firestore/src/core/structured_pipeline.ts | 74 +++--- .../firestore/src/lite-api/pipeline_impl.ts | 27 ++- .../src/lite-api/pipeline_settings.ts | 29 ++- .../firestore/src/util/input_validation.ts | 3 +- .../test/unit/api/pipeline_impl.test.ts | 2 +- .../unit/core/structured_pipeline.test.ts | 226 +++--------------- 7 files changed, 129 insertions(+), 257 deletions(-) diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts index c2183ca7e3a..e4c5744e867 100644 --- a/packages/firestore/src/api/pipeline_impl.ts +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -28,11 +28,9 @@ import { PipelineOptions } from '../lite-api/pipeline_settings'; import { Stage } from '../lite-api/stage'; import { newUserDataReader, - parseData, UserDataReader, UserDataSource } from '../lite-api/user_data_reader'; -import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; import { cast } from '../util/input_validation'; import { ensureFirestoreConfigured, Firestore } from './database'; @@ -84,17 +82,16 @@ export function execute(options: PipelineOptions): Promise; export function execute( pipelineOrOptions: LitePipeline | PipelineOptions ): Promise { - const pipeline: LitePipeline = - pipelineOrOptions instanceof LitePipeline - ? pipelineOrOptions - : pipelineOrOptions.pipeline; - const options: StructuredPipelineOptions = !( + const options: PipelineOptions = !( pipelineOrOptions instanceof LitePipeline ) ? pipelineOrOptions - : {}; - const genericOptions: { [name: string]: unknown } = - (pipelineOrOptions as PipelineOptions).genericOptions ?? {}; + : { + pipeline: pipelineOrOptions + }; + + const { pipeline, customOptions, ...rest } = options; + const genericOptions: { [name:string]: unknown } = customOptions ?? {}; const firestore = cast(pipeline._db, Firestore); const client = ensureFirestoreConfigured(firestore); @@ -104,13 +101,13 @@ export function execute( /* ignoreUndefinedProperties */ true ); const context = udr.createContext(UserDataSource.Argument, 'execute'); - const optionsOverride: ApiClientObjectMap = - parseData(genericOptions, context)?.mapValue?.fields ?? {}; + + const structuredPipelineOptions = new StructuredPipelineOptions(rest, genericOptions); + structuredPipelineOptions._readUserData(udr, context); const structuredPipeline: StructuredPipeline = new StructuredPipeline( pipeline, - options, - optionsOverride + structuredPipelineOptions ); return firestoreClientExecutePipeline(client, structuredPipeline).then( diff --git a/packages/firestore/src/core/structured_pipeline.ts b/packages/firestore/src/core/structured_pipeline.ts index ef913512275..66c26ae6a6a 100644 --- a/packages/firestore/src/core/structured_pipeline.ts +++ b/packages/firestore/src/core/structured_pipeline.ts @@ -15,19 +15,40 @@ * limitations under the License. */ -import { ObjectValue } from '../model/object_value'; -import { FieldPath } from '../model/path'; +import {ParseContext} from "../api/parse_context"; +import {UserDataReader, UserDataSource} from "../lite-api/user_data_reader"; import { - StructuredPipeline as StructuredPipelineProto, + ApiClientObjectMap, firestoreV1ApiClientInterfaces, Pipeline as PipelineProto, - ApiClientObjectMap, - Value + StructuredPipeline as StructuredPipelineProto } from '../protos/firestore_proto_api'; -import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; -import { mapToArray } from '../util/obj'; +import { + JsonProtoSerializer, + ProtoSerializable, + UserData +} from '../remote/serializer'; + +import {OptionsUtil} from "./options_util"; + +export class StructuredPipelineOptions implements UserData{ + proto: ApiClientObjectMap | undefined; + + readonly optionsUtil = new OptionsUtil({ + indexMode: { + serverName: 'index_mode', + } + }); -export interface StructuredPipelineOptions { - indexMode?: 'recommended'; + constructor( + private _userOptions: Record = {}, + private _optionsOverride: Record = {}) {} + + _readUserData(dataReader: UserDataReader, context?: ParseContext): void { + if (!context) { + context = dataReader.createContext(UserDataSource.Argument, "StructuredPipelineOptions._readUserData"); + } + this.proto = this.optionsUtil.getOptionsProto(context, this._userOptions, this._optionsOverride); + } } export class StructuredPipeline @@ -36,45 +57,12 @@ export class StructuredPipeline constructor( private pipeline: ProtoSerializable, private options: StructuredPipelineOptions, - private optionsOverride: ApiClientObjectMap ) {} - /** - * @private - * @internal for testing - */ - _getKnownOptions(): ObjectValue { - const options: ObjectValue = ObjectValue.empty(); - - // SERIALIZE KNOWN OPTIONS - if (typeof this.options.indexMode === 'string') { - options.set(FieldPath.fromServerFormat('index_mode'), { - stringValue: this.options.indexMode - }); - } - - return options; - } - - private getOptionsProto(): ApiClientObjectMap { - const options: ObjectValue = this._getKnownOptions(); - - // APPLY OPTIONS OVERRIDES - const optionsMap = new Map( - mapToArray(this.optionsOverride, (value, key) => [ - FieldPath.fromServerFormat(key), - value - ]) - ); - options.setAll(optionsMap); - - return options.value.mapValue.fields ?? {}; - } - _toProto(serializer: JsonProtoSerializer): StructuredPipelineProto { return { pipeline: this.pipeline._toProto(serializer), - options: this.getOptionsProto() + options: this.options.proto }; } } diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts index c1ca940a56b..530c9a61975 100644 --- a/packages/firestore/src/lite-api/pipeline_impl.ts +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -15,6 +15,10 @@ * limitations under the License. */ +import { + StructuredPipeline, + StructuredPipelineOptions +} from "../core/structured_pipeline"; import { invokeExecutePipeline } from '../remote/datastore'; import { getDatastore } from './components'; @@ -25,7 +29,11 @@ import { PipelineSource } from './pipeline-source'; import { DocumentReference } from './reference'; import { LiteUserDataWriter } from './reference_impl'; import { Stage } from './stage'; -import { newUserDataReader } from './user_data_reader'; +import { + newUserDataReader, + UserDataReader, + UserDataSource +} from './user_data_reader'; declare module './database' { interface Firestore { @@ -69,7 +77,22 @@ declare module './database' { */ export function execute(pipeline: Pipeline): Promise { const datastore = getDatastore(pipeline._db); - return invokeExecutePipeline(datastore, pipeline).then(result => { + + const udr = new UserDataReader( + pipeline._db._databaseId, + /* ignoreUndefinedProperties */ true + ); + const context = udr.createContext(UserDataSource.Argument, 'execute'); + + const structuredPipelineOptions = new StructuredPipelineOptions({}, {}); + structuredPipelineOptions._readUserData(udr, context); + + const structuredPipeline: StructuredPipeline = new StructuredPipeline( + pipeline, + structuredPipelineOptions + ); + + return invokeExecutePipeline(datastore, structuredPipeline).then(result => { // Get the execution time from the first result. // firestoreClientExecutePipeline returns at least one PipelineStreamElement // even if the returned document set is empty. diff --git a/packages/firestore/src/lite-api/pipeline_settings.ts b/packages/firestore/src/lite-api/pipeline_settings.ts index e23cc33b69a..0df2676f144 100644 --- a/packages/firestore/src/lite-api/pipeline_settings.ts +++ b/packages/firestore/src/lite-api/pipeline_settings.ts @@ -1,7 +1,20 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import type { Pipeline } from './pipeline'; /** - * Options defining how a Pipeline is evaluated. + * Options defining Pipeline execution. */ export interface PipelineOptions { /** @@ -18,21 +31,21 @@ export interface PipelineOptions { * An escape hatch to set options not known at SDK build time. These values * will be passed directly to the Firestore backend and not used by the SDK. * - * The generic option name will be used as provided. And must match the name + * The option name will be used as provided. And must match the name * format used by the backend (hint: use a snake_case_name). * - * Generic option values can be any type supported + * Custom option values can be any type supported * by Firestore (for example: string, boolean, number, map, …). Value types * not known to the SDK will be rejected. * - * Values specified in genericOptions will take precedence over any options + * Values specified in customOptions will take precedence over any options * with the same name set by the SDK. * * Override the `example_option`: * ``` * execute({ * pipeline: myPipeline, - * genericOptions: { + * customOptions: { * // Override `example_option`. This will not * // merge with the existing `example_option` object. * "example_option": { @@ -42,12 +55,12 @@ export interface PipelineOptions { * } * ``` * - * `genericOptions` supports dot notation, if you want to override + * `customOptions` supports dot notation, if you want to override * a nested option. * ``` * execute({ * pipeline: myPipeline, - * genericOptions: { + * customOptions: { * // Override `example_option.foo` and do not override * // any other properties of `example_option`. * "example_option.foo": "bar" @@ -55,7 +68,7 @@ export interface PipelineOptions { * } * ``` */ - genericOptions?: { + customOptions?: { [name: string]: unknown; }; } diff --git a/packages/firestore/src/util/input_validation.ts b/packages/firestore/src/util/input_validation.ts index 7fd9967b5a0..4973cb68bca 100644 --- a/packages/firestore/src/util/input_validation.ts +++ b/packages/firestore/src/util/input_validation.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import {DocumentData} from "../lite-api/reference"; import { DocumentKey } from '../model/document_key'; import { ResourcePath } from '../model/path'; @@ -92,7 +93,7 @@ export function validateCollectionPath(path: ResourcePath): void { * Returns true if it's a non-null object without a custom prototype * (i.e. excludes Array, Date, etc.). */ -export function isPlainObject(input: unknown): boolean { +export function isPlainObject(input: unknown): input is DocumentData { return ( typeof input === 'object' && input !== null && diff --git a/packages/firestore/test/unit/api/pipeline_impl.test.ts b/packages/firestore/test/unit/api/pipeline_impl.test.ts index c04d2cac6d5..39d592f45b0 100644 --- a/packages/firestore/test/unit/api/pipeline_impl.test.ts +++ b/packages/firestore/test/unit/api/pipeline_impl.test.ts @@ -165,7 +165,7 @@ describe('execute(Pipeline|PipelineOptions)', () => { await execute({ pipeline: firestore.pipeline().collection('foo'), - genericOptions: { + customOptions: { 'foo': 'bar' } }); diff --git a/packages/firestore/test/unit/core/structured_pipeline.test.ts b/packages/firestore/test/unit/core/structured_pipeline.test.ts index 920108df710..7603b895c80 100644 --- a/packages/firestore/test/unit/core/structured_pipeline.test.ts +++ b/packages/firestore/test/unit/core/structured_pipeline.test.ts @@ -19,20 +19,22 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { DatabaseId } from '../../../src/core/database_info'; -import { StructuredPipeline } from '../../../src/core/structured_pipeline'; -import { ObjectValue } from '../../../src/model/object_value'; +import {StructuredPipeline, StructuredPipelineOptions} from '../../../src/core/structured_pipeline'; import { Pipeline as PipelineProto } from '../../../src/protos/firestore_proto_api'; import { JsonProtoSerializer, ProtoSerializable } from '../../../src/remote/serializer'; +import {testUserDataReader} from "../../util/helpers"; -describe('StructuredPipeline', () => { +describe.only('StructuredPipeline', () => { it('should serialize the pipeline argument', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; - const structuredPipeline = new StructuredPipeline(pipeline, {}, {}); + const structuredPipelineOptions = new StructuredPipelineOptions(); + structuredPipelineOptions._readUserData(testUserDataReader(false)); + const structuredPipeline = new StructuredPipeline(pipeline, structuredPipelineOptions); const proto = structuredPipeline._toProto( new JsonProtoSerializer(DatabaseId.empty(), false) @@ -50,12 +52,13 @@ describe('StructuredPipeline', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; + + const options = new StructuredPipelineOptions({ + indexMode: 'recommended' + }); + options._readUserData(testUserDataReader(false)); const structuredPipeline = new StructuredPipeline( - pipeline, - { - indexMode: 'recommended' - }, - {} + pipeline,options ); const proto = structuredPipeline._toProto( @@ -78,12 +81,16 @@ describe('StructuredPipeline', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; + const options = + new StructuredPipelineOptions({}, + { + 'foo_bar': 'baz' + } + ); + options._readUserData(testUserDataReader(false)); const structuredPipeline = new StructuredPipeline( pipeline, - {}, - { - 'foo_bar': { stringValue: 'baz' } - } + options ); const proto = structuredPipeline._toProto( @@ -106,12 +113,16 @@ describe('StructuredPipeline', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; + const options = + new StructuredPipelineOptions({}, + { + 'foo.bar': 'baz' + } + ); + options._readUserData(testUserDataReader(false)); const structuredPipeline = new StructuredPipeline( pipeline, - {}, - { - 'foo.bar': { stringValue: 'baz' } - } + options ); const proto = structuredPipeline._toProto( @@ -138,14 +149,18 @@ describe('StructuredPipeline', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; + const options = + new StructuredPipelineOptions({ + indexMode: 'recommended' + }, + { + 'index_mode': 'baz' + } + ); + options._readUserData(testUserDataReader(false)); const structuredPipeline = new StructuredPipeline( pipeline, - { - indexMode: 'recommended' - }, - { - 'index_mode': { stringValue: 'baz' } - } + options ); const proto = structuredPipeline._toProto( @@ -163,169 +178,4 @@ describe('StructuredPipeline', () => { expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; }); - - it('should support options override of nested field', () => { - const pipeline: ProtoSerializable = { - _toProto: sinon.fake.returns({} as PipelineProto) - }; - - const structuredPipeline = new StructuredPipeline( - pipeline, - {}, - { - 'foo.bar': { integerValue: 123 } - } - ); - - // Fake known options with a nested {foo: {bar: "baz"}} - structuredPipeline._getKnownOptions = sinon.fake.returns( - new ObjectValue({ - mapValue: { - fields: { - 'foo': { - mapValue: { - fields: { - 'bar': { stringValue: 'baz' }, - 'waldo': { booleanValue: true } - } - } - } - } - } - }) - ); - - const proto = structuredPipeline._toProto( - new JsonProtoSerializer(DatabaseId.empty(), false) - ); - - expect(proto).to.deep.equal({ - pipeline: {}, - options: { - 'foo': { - mapValue: { - fields: { - 'bar': { - integerValue: 123 - }, - 'waldo': { - booleanValue: true - } - } - } - } - } - }); - - expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; - }); - - it('will replace a nested object if given a new object', () => { - const pipeline: ProtoSerializable = { - _toProto: sinon.fake.returns({} as PipelineProto) - }; - - const structuredPipeline = new StructuredPipeline( - pipeline, - {}, - { - 'foo': { mapValue: { fields: { bar: { integerValue: 123 } } } } - } - ); - - // Fake known options with a nested {foo: {bar: "baz"}} - structuredPipeline._getKnownOptions = sinon.fake.returns( - new ObjectValue({ - mapValue: { - fields: { - 'foo': { - mapValue: { - fields: { - 'bar': { stringValue: 'baz' }, - 'waldo': { booleanValue: true } - } - } - } - } - } - }) - ); - - const proto = structuredPipeline._toProto( - new JsonProtoSerializer(DatabaseId.empty(), false) - ); - - expect(proto).to.deep.equal({ - pipeline: {}, - options: { - 'foo': { - mapValue: { - fields: { - 'bar': { - integerValue: 123 - } - } - } - } - } - }); - - expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; - }); - - it('will replace a top level property that is not an object if given a nested field with dot notation', () => { - const pipeline: ProtoSerializable = { - _toProto: sinon.fake.returns({} as PipelineProto) - }; - - const structuredPipeline = new StructuredPipeline( - pipeline, - {}, - { - 'foo': { - mapValue: { - fields: { - 'bar': { stringValue: '123' }, - 'waldo': { booleanValue: true } - } - } - } - } - ); - - // Fake known options with a nested {foo: {bar: "baz"}} - structuredPipeline._getKnownOptions = sinon.fake.returns( - new ObjectValue({ - mapValue: { - fields: { - 'foo': { integerValue: 123 } - } - } - }) - ); - - const proto = structuredPipeline._toProto( - new JsonProtoSerializer(DatabaseId.empty(), false) - ); - - expect(proto).to.deep.equal({ - pipeline: {}, - options: { - 'foo': { - mapValue: { - fields: { - 'bar': { - stringValue: '123' - }, - 'waldo': { - booleanValue: true - } - } - } - } - } - }); - - expect((pipeline._toProto as sinon.SinonSpy).calledOnce).to.be.true; - }); }); From 5e9b24d771206ba0af8f09321b22e3828b2d4691 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 27 Aug 2025 12:19:09 -0600 Subject: [PATCH 05/25] OptionsUtil test --- .../test/unit/core/options_util.test.ts | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 packages/firestore/test/unit/core/options_util.test.ts diff --git a/packages/firestore/test/unit/core/options_util.test.ts b/packages/firestore/test/unit/core/options_util.test.ts new file mode 100644 index 00000000000..3e7ed530313 --- /dev/null +++ b/packages/firestore/test/unit/core/options_util.test.ts @@ -0,0 +1,228 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {expect} from "chai"; + +import {ParseContext} from "../../../src/api/parse_context"; +import {OptionsUtil} from "../../../src/core/options_util"; +import { + UserDataSource +} from "../../../src/lite-api/user_data_reader"; +import {testUserDataReader} from "../../util/helpers"; + +describe.only('OptionsUtil', () => { + let context: ParseContext | undefined; + beforeEach(async () => { + context = testUserDataReader(false).createContext(UserDataSource.Argument, 'beforeEach'); + }); + + afterEach(async () => { + context = undefined; + }); + + it('should support known options', () => { + const optionsUtil = new OptionsUtil({ + fooBar: { + serverName: 'foo_bar', + }, + }); + const proto = optionsUtil.getOptionsProto( + context!, { + fooBar: 'recommended', + }); + + expect(proto).deep.equal({ + 'foo_bar': { + stringValue: 'recommended', + }, + }); + }); + + it('should support unknown options', () => { + const optionsUtil = new OptionsUtil({}); + const proto = optionsUtil.getOptionsProto( + context!, + {}, + {baz: 'foo'} + ); + + expect(proto).to.deep.equal({ + baz: { + stringValue: 'foo', + }, + }); + }); + + it('should support unknown nested options', () => { + const optionsUtil = new OptionsUtil({}); + const proto = optionsUtil.getOptionsProto( + context!, + {}, + {'foo.bar': 'baz'} + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: {stringValue: 'baz'}, + }, + }, + }, + }); + }); + + it('should support options override', () => { + const optionsUtil = new OptionsUtil({ + indexMode: { + serverName: 'index_mode', + }, + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + indexMode: 'recommended', + }, + { + 'index_mode': 'baz', + } + ); + + expect(proto).to.deep.equal({ + 'index_mode': { + stringValue: 'baz', + }, + }); + }); + + it('should support options override of nested field', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo', + nestedOptions: { + bar: { + serverName: 'bar', + }, + waldo: { + serverName: 'waldo', + }, + }, + }, + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: {bar: 'yep', waldo: 'found'}, + }, + { + 'foo.bar': 123, + 'foo.baz': true, + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + integerValue: '123', + }, + waldo: { + stringValue: 'found', + }, + baz: { + booleanValue: true, + }, + }, + }, + }, + }); + }); + + it('will replace a nested object if given a new object', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo', + nestedOptions: { + bar: { + serverName: 'bar', + }, + waldo: { + serverName: 'waldo', + }, + }, + }, + }); + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: {bar: 'yep', waldo: 'found'}, + }, + { + foo: { + bar: 123, + }, + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + integerValue: '123', + }, + }, + }, + }, + }); + }); + + it('will replace a top level property that is not an object if given a nested field with dot notation', () => { + const optionsUtil = new OptionsUtil({ + foo: { + serverName: 'foo', + }, + }); + + const proto = optionsUtil.getOptionsProto( + context!, + { + foo: 'bar', + }, + { + 'foo.bar': '123', + 'foo.waldo': true, + } + ); + + expect(proto).to.deep.equal({ + foo: { + mapValue: { + fields: { + bar: { + stringValue: '123', + }, + waldo: { + booleanValue: true, + }, + }, + }, + }, + }); + }); +}); From 527131cda00bdc8034b1489ffc2ceead7358029a Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:02:33 -0600 Subject: [PATCH 06/25] Pipeline and stage options. Tangential renames and minor fixes --- .../firestore/lite/pipelines/pipelines.ts | 26 +- packages/firestore/src/api/pipeline_impl.ts | 13 +- packages/firestore/src/api_pipelines.ts | 29 +- packages/firestore/src/core/options_util.ts | 94 ++ packages/firestore/src/core/pipeline-util.ts | 3 +- .../firestore/src/core/structured_pipeline.ts | 8 +- .../firestore/src/lite-api/expressions.ts | 428 ++++--- .../firestore/src/lite-api/pipeline-source.ts | 209 +++- packages/firestore/src/lite-api/pipeline.ts | 1025 +++++++++++++---- .../firestore/src/lite-api/pipeline_impl.ts | 4 +- ...peline_settings.ts => pipeline_options.ts} | 0 packages/firestore/src/lite-api/stage.ts | 587 +++++++--- .../firestore/src/lite-api/stage_options.ts | 281 +++++ .../src/lite-api/user_data_reader.ts | 8 + packages/firestore/src/remote/serializer.ts | 5 - packages/firestore/src/util/pipeline_util.ts | 114 ++ packages/firestore/src/util/types.ts | 86 ++ .../test/integration/api/pipeline.test.ts | 402 ++++++- packages/firestore/test/lite/pipeline.test.ts | 43 +- .../test/unit/api/pipeline_impl.test.ts | 9 +- .../test/unit/core/options_util.test.ts | 2 +- .../unit/core/structured_pipeline.test.ts | 24 +- 22 files changed, 2621 insertions(+), 779 deletions(-) create mode 100644 packages/firestore/src/core/options_util.ts rename packages/firestore/src/lite-api/{pipeline_settings.ts => pipeline_options.ts} (100%) create mode 100644 packages/firestore/src/lite-api/stage_options.ts create mode 100644 packages/firestore/src/util/pipeline_util.ts diff --git a/packages/firestore/lite/pipelines/pipelines.ts b/packages/firestore/lite/pipelines/pipelines.ts index cc7f91e750c..b89a20cb3d3 100644 --- a/packages/firestore/lite/pipelines/pipelines.ts +++ b/packages/firestore/lite/pipelines/pipelines.ts @@ -50,6 +50,8 @@ export type { export { PipelineSource } from '../../src/lite-api/pipeline-source'; +export { OneOf } from '../../src/util/types'; + export { PipelineResult, PipelineSnapshot @@ -59,9 +61,30 @@ export { Pipeline } from '../../src/lite-api/pipeline'; export { execute } from '../../src/lite-api/pipeline_impl'; +export { + StageOptions, + CollectionStageOptions, + CollectionGroupStageOptions, + DatabaseStageOptions, + DocumentsStageOptions, + AddFieldsStageOptions, + RemoveFieldsStageOptions, + SelectStageOptions, + WhereStageOptions, + OffsetStageOptions, + LimitStageOptions, + DistinctStageOptions, + AggregateStageOptions, + FindNearestStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, + UnionStageOptions, + UnnestStageOptions, + SortStageOptions +} from '../../src/lite-api/stage_options'; + export { Stage, - FindNearestOptions, AddFields, Aggregate, Distinct, @@ -83,7 +106,6 @@ export { field, and, array, - arrayOffset, constant, add, subtract, diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts index e4c5744e867..ce415002a63 100644 --- a/packages/firestore/src/api/pipeline_impl.ts +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -24,7 +24,7 @@ import { import { Pipeline as LitePipeline } from '../lite-api/pipeline'; import { PipelineResult, PipelineSnapshot } from '../lite-api/pipeline-result'; import { PipelineSource } from '../lite-api/pipeline-source'; -import { PipelineOptions } from '../lite-api/pipeline_settings'; +import { PipelineOptions } from '../lite-api/pipeline_options'; import { Stage } from '../lite-api/stage'; import { newUserDataReader, @@ -91,7 +91,6 @@ export function execute( }; const { pipeline, customOptions, ...rest } = options; - const genericOptions: { [name:string]: unknown } = customOptions ?? {}; const firestore = cast(pipeline._db, Firestore); const client = ensureFirestoreConfigured(firestore); @@ -102,8 +101,8 @@ export function execute( ); const context = udr.createContext(UserDataSource.Argument, 'execute'); - const structuredPipelineOptions = new StructuredPipelineOptions(rest, genericOptions); - structuredPipelineOptions._readUserData(udr, context); + const structuredPipelineOptions = new StructuredPipelineOptions(rest, customOptions); + structuredPipelineOptions._readUserData(context); const structuredPipeline: StructuredPipeline = new StructuredPipeline( pipeline, @@ -142,10 +141,12 @@ export function execute( // Augment the Firestore class with the pipeline() factory method Firestore.prototype.pipeline = function (): PipelineSource { - return new PipelineSource(this._databaseId, (stages: Stage[]) => { + const userDataReader = + newUserDataReader(this); + return new PipelineSource(this._databaseId, userDataReader, (stages: Stage[]) => { return new Pipeline( this, - newUserDataReader(this), + userDataReader, new ExpUserDataWriter(this), stages ); diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index b632025f374..d2b221c08b0 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -17,6 +17,8 @@ export { PipelineSource } from './lite-api/pipeline-source'; +export { OneOf } from './util/types'; + export { PipelineResult, PipelineSnapshot, @@ -27,9 +29,32 @@ export { Pipeline } from './api/pipeline'; export { execute } from './api/pipeline_impl'; +export { PipelineOptions } from './lite-api/pipeline_options'; + +export { + StageOptions, + CollectionStageOptions, + CollectionGroupStageOptions, + DatabaseStageOptions, + DocumentsStageOptions, + AddFieldsStageOptions, + RemoveFieldsStageOptions, + SelectStageOptions, + WhereStageOptions, + OffsetStageOptions, + LimitStageOptions, + DistinctStageOptions, + AggregateStageOptions, + FindNearestStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, + UnionStageOptions, + UnnestStageOptions, + SortStageOptions +} from './lite-api/stage_options'; + export { Stage, - FindNearestOptions, AddFields, Aggregate, Distinct, @@ -121,7 +146,7 @@ export { bitRightShift, rand, array, - arrayOffset, + arrayGet, isError, ifError, isAbsent, diff --git a/packages/firestore/src/core/options_util.ts b/packages/firestore/src/core/options_util.ts new file mode 100644 index 00000000000..b2b9adb6e45 --- /dev/null +++ b/packages/firestore/src/core/options_util.ts @@ -0,0 +1,94 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {ParseContext} from "../api/parse_context"; +import { + parseData, +} from "../lite-api/user_data_reader"; +import { ObjectValue } from '../model/object_value'; +import { FieldPath } from '../model/path'; +import {ApiClientObjectMap, Value} from "../protos/firestore_proto_api"; +import {isPlainObject} from "../util/input_validation"; +import { mapToArray } from '../util/obj'; +export type OptionsDefinitions = Record; +export interface OptionDefinition { + serverName: string; + nestedOptions?: OptionsDefinitions; +} + +export class OptionsUtil { + constructor(private optionDefinitions: OptionsDefinitions) {} + + private _getKnownOptions( + options: Record, + context: ParseContext + ): ObjectValue { + const knownOptions: ObjectValue = ObjectValue.empty(); + + // SERIALIZE KNOWN OPTIONS + for (const knownOptionKey in this.optionDefinitions) { + if (this.optionDefinitions.hasOwnProperty(knownOptionKey)) { + const optionDefinition: OptionDefinition = + this.optionDefinitions[knownOptionKey]; + + if (knownOptionKey in options) { + const optionValue: unknown = options[knownOptionKey]; + let protoValue: Value | undefined = undefined; + + if (optionDefinition.nestedOptions && isPlainObject(optionValue)) { + const nestedUtil = new OptionsUtil(optionDefinition.nestedOptions); + protoValue = { + mapValue: { + fields: nestedUtil.getOptionsProto(context, optionValue), + }, + }; + } else if (optionValue) { + protoValue = parseData(optionValue, context) ?? undefined; + } + + if (protoValue) { + knownOptions.set( + FieldPath.fromServerFormat(optionDefinition.serverName), + protoValue + ); + } + } + } + } + + return knownOptions; + } + + getOptionsProto( + context: ParseContext, + knownOptions: Record, + optionsOverride?: Record + ): ApiClientObjectMap | undefined { + const result: ObjectValue = this._getKnownOptions(knownOptions, context); + + // APPLY OPTIONS OVERRIDES + if (optionsOverride) { + const optionsMap = new Map( + mapToArray(optionsOverride, (value, key) => [ + FieldPath.fromServerFormat(key), + value !== undefined ? parseData(value, context) : null, + ]) + ); + result.setAll(optionsMap); + } + + // Return MapValue from `result` or empty map value + return result.value.mapValue.fields ?? {}; + } +} diff --git a/packages/firestore/src/core/pipeline-util.ts b/packages/firestore/src/core/pipeline-util.ts index cb3342eca4e..7b810f091a2 100644 --- a/packages/firestore/src/core/pipeline-util.ts +++ b/packages/firestore/src/core/pipeline-util.ts @@ -161,7 +161,8 @@ function reverseOrderings(orderings: Ordering[]): Ordering[] { o => new Ordering( o.expr, - o.direction === 'ascending' ? 'descending' : 'ascending' + o.direction === 'ascending' ? 'descending' : 'ascending', + undefined ) ); } diff --git a/packages/firestore/src/core/structured_pipeline.ts b/packages/firestore/src/core/structured_pipeline.ts index 66c26ae6a6a..b020db06e0a 100644 --- a/packages/firestore/src/core/structured_pipeline.ts +++ b/packages/firestore/src/core/structured_pipeline.ts @@ -16,7 +16,7 @@ */ import {ParseContext} from "../api/parse_context"; -import {UserDataReader, UserDataSource} from "../lite-api/user_data_reader"; +import {UserData} from "../lite-api/user_data_reader"; import { ApiClientObjectMap, firestoreV1ApiClientInterfaces, Pipeline as PipelineProto, @@ -25,7 +25,6 @@ import { import { JsonProtoSerializer, ProtoSerializable, - UserData } from '../remote/serializer'; import {OptionsUtil} from "./options_util"; @@ -43,10 +42,7 @@ export class StructuredPipelineOptions implements UserData{ private _userOptions: Record = {}, private _optionsOverride: Record = {}) {} - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - if (!context) { - context = dataReader.createContext(UserDataSource.Argument, "StructuredPipelineOptions._readUserData"); - } + _readUserData(context: ParseContext): void { this.proto = this.optionsUtil.getOptionsProto(context, this._userOptions, this._optionsOverride); } } diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index fa0fbf68064..8bd64a3680a 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -26,7 +26,6 @@ import { ProtoValueSerializable, toMapValue, toStringValue, - UserData } from '../remote/serializer'; import { hardAssert } from '../util/assert'; import { isPlainObject } from '../util/input_validation'; @@ -40,9 +39,7 @@ import { DocumentReference } from './reference'; import { Timestamp } from './timestamp'; import { fieldPathFromArgument, - parseData, - UserDataReader, - UserDataSource + parseData, UserData, } from './user_data_reader'; import { VectorValue } from './vector_value'; @@ -72,14 +69,13 @@ function valueToDefaultExpr(value: unknown): Expr { if (value instanceof Expr) { return value; } else if (isPlainObject(value)) { - result = map(value as Record); + result = _map(value as Record, undefined); } else if (value instanceof Array) { result = array(value); } else { - result = new Constant(value); + result = _constant(value, undefined); } - result._createdFromLiteral = true; return result; } @@ -96,7 +92,6 @@ function vectorToExpr(value: VectorValue | number[] | Expr): Expr { return value; } else { const result = constantVector(value); - result._createdFromLiteral = true; return result; } } @@ -114,7 +109,6 @@ function vectorToExpr(value: VectorValue | number[] | Expr): Expr { function fieldOrExpression(value: unknown): Expr { if (isString(value)) { const result = field(value); - result._createdFromLiteral = true; return result; } else { return valueToDefaultExpr(value); @@ -140,13 +134,7 @@ function fieldOrExpression(value: unknown): Expr { export abstract class Expr implements ProtoValueSerializable, UserData { abstract readonly exprType: ExprType; - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; + abstract readonly _methodName?: string; /** * @private @@ -160,8 +148,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @internal */ abstract _readUserData( - dataReader: UserDataReader, - context?: ParseContext + context: ParseContext ): void; /** @@ -176,12 +163,11 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param others Optional additional expressions or literals to add to this expression. * @return A new `Expr` representing the addition operation. */ - add(second: Expr | unknown, ...others: Array): FunctionExpr { - const values = [second, ...others]; + add(second: Expr | unknown): FunctionExpr { return new FunctionExpr('add', [ this, - ...values.map(value => valueToDefaultExpr(value)) - ]); + valueToDefaultExpr(second) + ], 'add'); } /** @@ -210,7 +196,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ subtract(other: number): FunctionExpr; subtract(other: number | Expr): FunctionExpr { - return new FunctionExpr('subtract', [this, valueToDefaultExpr(other)]); + return new FunctionExpr('subtract', [this, valueToDefaultExpr(other)], 'subtract'); } /** @@ -227,13 +213,11 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ multiply( second: Expr | number, - ...others: Array ): FunctionExpr { return new FunctionExpr('multiply', [ this, - valueToDefaultExpr(second), - ...others.map(value => valueToDefaultExpr(value)) - ]); + valueToDefaultExpr(second) + ], 'multiply'); } /** @@ -262,7 +246,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ divide(other: number): FunctionExpr; divide(other: number | Expr): FunctionExpr { - return new FunctionExpr('divide', [this, valueToDefaultExpr(other)]); + return new FunctionExpr('divide', [this, valueToDefaultExpr(other)], 'divide'); } /** @@ -291,7 +275,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ mod(value: number): FunctionExpr; mod(other: number | Expr): FunctionExpr { - return new FunctionExpr('mod', [this, valueToDefaultExpr(other)]); + return new FunctionExpr('mod', [this, valueToDefaultExpr(other)], 'mod'); } /** @@ -320,7 +304,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ eq(value: unknown): BooleanExpr; eq(other: unknown): BooleanExpr { - return new BooleanExpr('eq', [this, valueToDefaultExpr(other)]); + return new BooleanExpr('eq', [this, valueToDefaultExpr(other)], 'eq'); } /** @@ -349,7 +333,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ neq(value: unknown): BooleanExpr; neq(other: unknown): BooleanExpr { - return new BooleanExpr('neq', [this, valueToDefaultExpr(other)]); + return new BooleanExpr('neq', [this, valueToDefaultExpr(other)], 'neq'); } /** @@ -378,7 +362,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ lt(value: unknown): BooleanExpr; lt(other: unknown): BooleanExpr { - return new BooleanExpr('lt', [this, valueToDefaultExpr(other)]); + return new BooleanExpr('lt', [this, valueToDefaultExpr(other)], 'lt'); } /** @@ -408,7 +392,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ lte(value: unknown): BooleanExpr; lte(other: unknown): BooleanExpr { - return new BooleanExpr('lte', [this, valueToDefaultExpr(other)]); + return new BooleanExpr('lte', [this, valueToDefaultExpr(other)], 'lte'); } /** @@ -437,7 +421,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ gt(value: unknown): BooleanExpr; gt(other: unknown): BooleanExpr { - return new BooleanExpr('gt', [this, valueToDefaultExpr(other)]); + return new BooleanExpr('gt', [this, valueToDefaultExpr(other)], 'gt'); } /** @@ -468,7 +452,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ gte(value: unknown): BooleanExpr; gte(other: unknown): BooleanExpr { - return new BooleanExpr('gte', [this, valueToDefaultExpr(other)]); + return new BooleanExpr('gte', [this, valueToDefaultExpr(other)], 'gte'); } /** @@ -488,7 +472,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { ): FunctionExpr { const elements = [secondArray, ...otherArrays]; const exprValues = elements.map(value => valueToDefaultExpr(value)); - return new FunctionExpr('array_concat', [this, ...exprValues]); + return new FunctionExpr('array_concat', [this, ...exprValues], 'arrayConcat'); } /** @@ -520,7 +504,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new BooleanExpr('array_contains', [ this, valueToDefaultExpr(element) - ]); + ], 'arrayContains'); } /** @@ -550,9 +534,9 @@ export abstract class Expr implements ProtoValueSerializable, UserData { arrayContainsAll(arrayExpression: Expr): BooleanExpr; arrayContainsAll(values: unknown[] | Expr): BooleanExpr { const normalizedExpr = Array.isArray(values) - ? new ListOfExprs(values.map(valueToDefaultExpr)) + ? new ListOfExprs(values.map(valueToDefaultExpr), 'arrayContainsAll') : values; - return new BooleanExpr('array_contains_all', [this, normalizedExpr]); + return new BooleanExpr('array_contains_all', [this, normalizedExpr], 'arrayContainsAll'); } /** @@ -583,9 +567,9 @@ export abstract class Expr implements ProtoValueSerializable, UserData { arrayContainsAny(arrayExpression: Expr): BooleanExpr; arrayContainsAny(values: Array | Expr): BooleanExpr { const normalizedExpr = Array.isArray(values) - ? new ListOfExprs(values.map(valueToDefaultExpr)) + ? new ListOfExprs(values.map(valueToDefaultExpr), 'arrayContainsAny') : values; - return new BooleanExpr('array_contains_any', [this, normalizedExpr]); + return new BooleanExpr('array_contains_any', [this, normalizedExpr], 'arrayContainsAny'); } /** @@ -599,7 +583,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the length of the array. */ arrayLength(): FunctionExpr { - return new FunctionExpr('array_length', [this]); + return new FunctionExpr('array_length', [this], 'arrayLength'); } /** @@ -631,9 +615,9 @@ export abstract class Expr implements ProtoValueSerializable, UserData { eqAny(arrayExpression: Expr): BooleanExpr; eqAny(others: unknown[] | Expr): BooleanExpr { const exprOthers = Array.isArray(others) - ? new ListOfExprs(others.map(valueToDefaultExpr)) + ? new ListOfExprs(others.map(valueToDefaultExpr), 'eqAny') : others; - return new BooleanExpr('eq_any', [this, exprOthers]); + return new BooleanExpr('eq_any', [this, exprOthers], 'eqAny'); } /** @@ -664,9 +648,9 @@ export abstract class Expr implements ProtoValueSerializable, UserData { notEqAny(arrayExpression: Expr): BooleanExpr; notEqAny(others: unknown[] | Expr): BooleanExpr { const exprOthers = Array.isArray(others) - ? new ListOfExprs(others.map(valueToDefaultExpr)) + ? new ListOfExprs(others.map(valueToDefaultExpr), 'notEqAny') : others; - return new BooleanExpr('not_eq_any', [this, exprOthers]); + return new BooleanExpr('not_eq_any', [this, exprOthers], 'notEqAny'); } /** @@ -680,7 +664,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the 'isNaN' check. */ isNan(): BooleanExpr { - return new BooleanExpr('is_nan', [this]); + return new BooleanExpr('is_nan', [this], 'isNan'); } /** @@ -694,7 +678,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the 'isNull' check. */ isNull(): BooleanExpr { - return new BooleanExpr('is_null', [this]); + return new BooleanExpr('is_null', [this], 'isNull'); } /** @@ -708,7 +692,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the 'exists' check. */ exists(): BooleanExpr { - return new BooleanExpr('exists', [this]); + return new BooleanExpr('exists', [this], 'exists'); } /** @@ -722,7 +706,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the length of the string. */ charLength(): FunctionExpr { - return new FunctionExpr('char_length', [this]); + return new FunctionExpr('char_length', [this], 'charLength'); } /** @@ -736,7 +720,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new `Expr` representing the 'like' comparison. */ - like(pattern: string): FunctionExpr; + like(pattern: string): BooleanExpr; /** * Creates an expression that performs a case-sensitive string comparison. @@ -749,9 +733,9 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new `Expr` representing the 'like' comparison. */ - like(pattern: Expr): FunctionExpr; - like(stringOrExpr: string | Expr): FunctionExpr { - return new FunctionExpr('like', [this, valueToDefaultExpr(stringOrExpr)]); + like(pattern: Expr): BooleanExpr; + like(stringOrExpr: string | Expr): BooleanExpr { + return new BooleanExpr('like', [this, valueToDefaultExpr(stringOrExpr)], 'like'); } /** @@ -785,7 +769,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new BooleanExpr('regex_contains', [ this, valueToDefaultExpr(stringOrExpr) - ]); + ], 'regexContains'); } /** @@ -817,7 +801,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new BooleanExpr('regex_match', [ this, valueToDefaultExpr(stringOrExpr) - ]); + ], 'regexMatch'); } /** @@ -849,7 +833,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new BooleanExpr('str_contains', [ this, valueToDefaultExpr(stringOrExpr) - ]); + ], 'strContains'); } /** @@ -882,7 +866,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new BooleanExpr('starts_with', [ this, valueToDefaultExpr(stringOrExpr) - ]); + ], 'startsWith'); } /** @@ -915,7 +899,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new BooleanExpr('ends_with', [ this, valueToDefaultExpr(stringOrExpr) - ]); + ], 'endsWith'); } /** @@ -929,7 +913,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the lowercase string. */ toLower(): FunctionExpr { - return new FunctionExpr('to_lower', [this]); + return new FunctionExpr('to_lower', [this], 'toLower'); } /** @@ -943,7 +927,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the uppercase string. */ toUpper(): FunctionExpr { - return new FunctionExpr('to_upper', [this]); + return new FunctionExpr('to_upper', [this], 'toUpper'); } /** @@ -957,7 +941,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the trimmed string. */ trim(): FunctionExpr { - return new FunctionExpr('trim', [this]); + return new FunctionExpr('trim', [this], 'trim'); } /** @@ -978,7 +962,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { ): FunctionExpr { const elements = [secondString, ...otherStrings]; const exprs = elements.map(valueToDefaultExpr); - return new FunctionExpr('str_concat', [this, ...exprs]); + return new FunctionExpr('str_concat', [this, ...exprs], 'strConcat'); } /** @@ -992,7 +976,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the reversed string. */ reverse(): FunctionExpr { - return new FunctionExpr('reverse', [this]); + return new FunctionExpr('reverse', [this], 'reverse'); } /** @@ -1028,7 +1012,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { this, valueToDefaultExpr(find), valueToDefaultExpr(replace) - ]); + ], 'replaceFirst'); } /** @@ -1064,7 +1048,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { this, valueToDefaultExpr(find), valueToDefaultExpr(replace) - ]); + ], 'replaceAll'); } /** @@ -1078,7 +1062,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the length of the string in bytes. */ byteLength(): FunctionExpr { - return new FunctionExpr('byte_length', [this]); + return new FunctionExpr('byte_length', [this], 'byteLength'); } /** @@ -1093,7 +1077,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the value associated with the given key in the map. */ mapGet(subfield: string): FunctionExpr { - return new FunctionExpr('map_get', [this, constant(subfield)]); + return new FunctionExpr('map_get', [this, constant(subfield)], 'mapGet'); } /** @@ -1108,7 +1092,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'count' aggregation. */ count(): AggregateFunction { - return new AggregateFunction('count', [this]); + return new AggregateFunction('count', [this], 'count'); } /** @@ -1122,7 +1106,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'sum' aggregation. */ sum(): AggregateFunction { - return new AggregateFunction('sum', [this]); + return new AggregateFunction('sum', [this], 'sum'); } /** @@ -1137,7 +1121,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'avg' aggregation. */ avg(): AggregateFunction { - return new AggregateFunction('avg', [this]); + return new AggregateFunction('avg', [this], 'avg'); } /** @@ -1151,7 +1135,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'min' aggregation. */ minimum(): AggregateFunction { - return new AggregateFunction('minimum', [this]); + return new AggregateFunction('min', [this], 'minimum'); } /** @@ -1165,7 +1149,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'max' aggregation. */ maximum(): AggregateFunction { - return new AggregateFunction('maximum', [this]); + return new AggregateFunction('max', [this], 'maximum'); } /** @@ -1185,10 +1169,11 @@ export abstract class Expr implements ProtoValueSerializable, UserData { ...others: Array ): FunctionExpr { const values = [second, ...others]; - return new FunctionExpr('logical_maximum', [ + return new FunctionExpr('max', [ this, ...values.map(valueToDefaultExpr) - ]); + ], + 'logicalMaximum'); } /** @@ -1208,10 +1193,10 @@ export abstract class Expr implements ProtoValueSerializable, UserData { ...others: Array ): FunctionExpr { const values = [second, ...others]; - return new FunctionExpr('logical_minimum', [ + return new FunctionExpr('min', [ this, ...values.map(valueToDefaultExpr) - ]); + ], 'logicalMinimum'); } /** @@ -1225,7 +1210,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the length of the vector. */ vectorLength(): FunctionExpr { - return new FunctionExpr('vector_length', [this]); + return new FunctionExpr('vector_length', [this], 'vectorLength'); } /** @@ -1253,7 +1238,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ cosineDistance(vector: VectorValue | number[]): FunctionExpr; cosineDistance(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('cosine_distance', [this, vectorToExpr(other)]); + return new FunctionExpr('cosine_distance', [this, vectorToExpr(other)], 'cosineDistance'); } /** @@ -1282,7 +1267,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ dotProduct(vector: VectorValue | number[]): FunctionExpr; dotProduct(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('dot_product', [this, vectorToExpr(other)]); + return new FunctionExpr('dot_product', [this, vectorToExpr(other)], 'dotProduct'); } /** @@ -1311,7 +1296,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ euclideanDistance(vector: VectorValue | number[]): FunctionExpr; euclideanDistance(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('euclidean_distance', [this, vectorToExpr(other)]); + return new FunctionExpr('euclidean_distance', [this, vectorToExpr(other)], 'euclideanDistance'); } /** @@ -1326,7 +1311,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the timestamp. */ unixMicrosToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_micros_to_timestamp', [this]); + return new FunctionExpr('unix_micros_to_timestamp', [this], 'unixMicrosToTimestamp'); } /** @@ -1340,7 +1325,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the number of microseconds since epoch. */ timestampToUnixMicros(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_micros', [this]); + return new FunctionExpr('timestamp_to_unix_micros', [this], 'timestampToUnixMicros'); } /** @@ -1355,7 +1340,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the timestamp. */ unixMillisToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_millis_to_timestamp', [this]); + return new FunctionExpr('unix_millis_to_timestamp', [this], 'unixMillisToTimestamp'); } /** @@ -1369,7 +1354,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the number of milliseconds since epoch. */ timestampToUnixMillis(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_millis', [this]); + return new FunctionExpr('timestamp_to_unix_millis', [this], 'timestampToUnixMillis'); } /** @@ -1384,7 +1369,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the timestamp. */ unixSecondsToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_seconds_to_timestamp', [this]); + return new FunctionExpr('unix_seconds_to_timestamp', [this], 'unixSecondsToTimestamp'); } /** @@ -1398,7 +1383,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the number of seconds since epoch. */ timestampToUnixSeconds(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_seconds', [this]); + return new FunctionExpr('timestamp_to_unix_seconds', [this], 'timestampToUnixSeconds'); } /** @@ -1446,7 +1431,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { this, valueToDefaultExpr(unit), valueToDefaultExpr(amount) - ]); + ], 'timestampAdd'); } /** @@ -1494,7 +1479,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { this, valueToDefaultExpr(unit), valueToDefaultExpr(amount) - ]); + ], 'timestampSub'); } /** @@ -1529,7 +1514,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new FunctionExpr('bit_and', [ this, valueToDefaultExpr(bitsOrExpression) - ]); + ], 'bitAnd'); } /** @@ -1564,7 +1549,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new FunctionExpr('bit_or', [ this, valueToDefaultExpr(bitsOrExpression) - ]); + ], 'bitOr'); } /** @@ -1599,7 +1584,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new FunctionExpr('bit_xor', [ this, valueToDefaultExpr(bitsOrExpression) - ]); + ], 'bitXor'); } /** @@ -1615,7 +1600,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the bitwise NOT operation. */ bitNot(): FunctionExpr { - return new FunctionExpr('bit_not', [this]); + return new FunctionExpr('bit_not', [this], 'bitNot'); } /** @@ -1650,7 +1635,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new FunctionExpr('bit_left_shift', [ this, valueToDefaultExpr(numberExpr) - ]); + ], 'bitLeftShift'); } /** @@ -1685,7 +1670,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new FunctionExpr('bit_right_shift', [ this, valueToDefaultExpr(numberExpr) - ]); + ], 'bitRightShift'); } /** @@ -1701,7 +1686,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the documentId operation. */ documentId(): FunctionExpr { - return new FunctionExpr('document_id', [this]); + return new FunctionExpr('document_id', [this], 'documentId'); } /** @@ -1728,13 +1713,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { substr(position: Expr | number, length?: Expr | number): FunctionExpr { const positionExpr = valueToDefaultExpr(position); if (length === undefined) { - return new FunctionExpr('substr', [this, positionExpr]); + return new FunctionExpr('substr', [this, positionExpr], 'substr'); } else { return new FunctionExpr('substr', [ this, positionExpr, valueToDefaultExpr(length) - ]); + ], 'substr'); } } @@ -1746,13 +1731,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Return the value in the 'tags' field array at index `1`. - * field('tags').arrayOffset(1); + * field('tags').arrayGet(1); * ``` * * @param offset The index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ - arrayOffset(offset: number): FunctionExpr; + arrayGet(offset: number): FunctionExpr; /** * @beta @@ -1763,15 +1748,15 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ```typescript * // Return the value in the tags field array at index specified by field * // 'favoriteTag'. - * field('tags').arrayOffset(field('favoriteTag')); + * field('tags').arrayGet(field('favoriteTag')); * ``` * * @param offsetExpr An Expr evaluating to the index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ - arrayOffset(offsetExpr: Expr): FunctionExpr; - arrayOffset(offset: Expr | number): FunctionExpr { - return new FunctionExpr('array_offset', [this, valueToDefaultExpr(offset)]); + arrayGet(offsetExpr: Expr): FunctionExpr; + arrayGet(offset: Expr | number): FunctionExpr { + return new FunctionExpr('array_get', [this, valueToDefaultExpr(offset)], 'arrayGet'); } /** @@ -1787,7 +1772,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code BooleanExpr} representing the 'isError' check. */ isError(): BooleanExpr { - return new BooleanExpr('is_error', [this]); + return new BooleanExpr('is_error', [this], 'isError'); } /** @@ -1799,7 +1784,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ```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")); + * field("title").arrayGet(0).ifError(field("title")); * ``` * * @param catchExpr The catch expression that will be evaluated and @@ -1817,7 +1802,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ```typescript * // Returns the first item in the title field arrays, or returns * // "Default Title" - * field("title").arrayOffset(0).ifError("Default Title"); + * field("title").arrayGet(0).ifError("Default Title"); * ``` * * @param catchValue The value that will be returned if this expression @@ -1826,7 +1811,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ ifError(catchValue: unknown): FunctionExpr; ifError(catchValue: unknown): FunctionExpr { - return new FunctionExpr('if_error', [this, valueToDefaultExpr(catchValue)]); + return new FunctionExpr('if_error', [this, valueToDefaultExpr(catchValue)], 'ifError'); } /** @@ -1843,7 +1828,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code BooleanExpr} representing the 'isAbsent' check. */ isAbsent(): BooleanExpr { - return new BooleanExpr('is_absent', [this]); + return new BooleanExpr('is_absent', [this], 'isAbsent'); } /** @@ -1859,7 +1844,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code BooleanExpr} representing the 'isNotNull' check. */ isNotNull(): BooleanExpr { - return new BooleanExpr('is_not_null', [this]); + return new BooleanExpr('is_not_null', [this], 'isNotNull'); } /** @@ -1875,7 +1860,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new {@code Expr} representing the 'isNaN' check. */ isNotNan(): BooleanExpr { - return new BooleanExpr('is_not_nan', [this]); + return new BooleanExpr('is_not_nan', [this], 'isNotNan'); } /** @@ -1910,7 +1895,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { return new FunctionExpr('map_remove', [ this, valueToDefaultExpr(stringExpr) - ]); + ], 'mapRemove'); } /** @@ -1941,7 +1926,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { this, secondMapExpr, ...otherMapExprs - ]); + ], 'mapMerge'); } /** @@ -1991,7 +1976,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * expression and associates it with the provided alias. */ as(name: string): ExprWithAlias { - return new ExprWithAlias(this, name); + return new ExprWithAlias(this, name, 'as'); } } @@ -2014,15 +1999,9 @@ export interface Selectable { export class AggregateFunction implements ProtoValueSerializable, UserData { exprType: ExprType = 'AggregateFunction'; - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; - - constructor(private name: string, private params: Expr[]) {} + constructor( name: string, params: Expr[]); + constructor( name: string, params: Expr[], _methodName: string | undefined); + constructor(private name: string, private params: Expr[], readonly _methodName?: string) {} /** * Assigns an alias to this AggregateFunction. The alias specifies the name that @@ -2039,7 +2018,7 @@ export class AggregateFunction implements ProtoValueSerializable, UserData { * AggregateFunction and associates it with the provided alias. */ as(name: string): AggregateWithAlias { - return new AggregateWithAlias(this, name); + return new AggregateWithAlias(this, name, 'as'); } /** @@ -2061,13 +2040,10 @@ export class AggregateFunction implements ProtoValueSerializable, UserData { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, this.name); + _readUserData(context: ParseContext): void { + context = this._methodName ? context.contextWith({methodName: this._methodName}) : context; this.params.forEach(expr => { - return expr._readUserData(dataReader, context); + return expr._readUserData(context); }); } } @@ -2078,7 +2054,7 @@ export class AggregateFunction implements ProtoValueSerializable, UserData { * An AggregateFunction with alias. */ export class AggregateWithAlias implements UserData { - constructor(readonly aggregate: AggregateFunction, readonly alias: string) {} + constructor(readonly aggregate: AggregateFunction, readonly alias: string, readonly _methodName: string | undefined) {} /** * @internal @@ -2092,12 +2068,8 @@ export class AggregateWithAlias implements UserData { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, 'as'); - this.aggregate._readUserData(dataReader, context); + _readUserData(context: ParseContext): void { + this.aggregate._readUserData(context); } } @@ -2116,18 +2088,14 @@ export class ExprWithAlias implements Selectable, UserData { */ _createdFromLiteral: boolean = false; - constructor(readonly expr: Expr, readonly alias: string) {} + constructor(readonly expr: Expr, readonly alias: string, readonly _methodName: string | undefined) {} /** * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, 'as'); - this.expr._readUserData(dataReader, context); + _readUserData(context: ParseContext): void { + this.expr._readUserData(context); } } @@ -2137,7 +2105,7 @@ export class ExprWithAlias implements Selectable, UserData { class ListOfExprs extends Expr implements UserData { exprType: ExprType = 'ListOfExprs'; - constructor(private exprs: Expr[]) { + constructor(private exprs: Expr[], readonly _methodName: string | undefined) { super(); } @@ -2157,8 +2125,8 @@ class ListOfExprs extends Expr implements UserData { * @private * @internal */ - _readUserData(dataReader: UserDataReader): void { - this.exprs.forEach((expr: Expr) => expr._readUserData(dataReader)); + _readUserData(context: ParseContext): void { + this.exprs.forEach((expr: Expr) => expr._readUserData(context)); } } @@ -2190,7 +2158,7 @@ export class Field extends Expr implements Selectable { * @hideconstructor * @param fieldPath */ - constructor(private fieldPath: InternalFieldPath) { + constructor(private fieldPath: InternalFieldPath, readonly _methodName: string | undefined) { super(); } @@ -2220,7 +2188,7 @@ export class Field extends Expr implements Selectable { * @private * @internal */ - _readUserData(dataReader: UserDataReader): void {} + _readUserData(context: ParseContext): void {} } /** @@ -2243,13 +2211,17 @@ export class Field extends Expr implements Selectable { export function field(name: string): Field; export function field(path: FieldPath): Field; export function field(nameOrPath: string | FieldPath): Field { + return _field(nameOrPath, 'field'); +} + +export function _field(nameOrPath: string | FieldPath, methodName: string | undefined): Field { if (typeof nameOrPath === 'string') { if (DOCUMENT_KEY_NAME === nameOrPath) { - return new Field(documentIdFieldPath()._internalPath); + return new Field(documentIdFieldPath()._internalPath, methodName); } - return new Field(fieldPathFromArgument('field', nameOrPath)); + return new Field(fieldPathFromArgument('field', nameOrPath), methodName); } else { - return new Field(nameOrPath._internalPath); + return new Field(nameOrPath._internalPath, methodName); } } @@ -2279,7 +2251,7 @@ export class Constant extends Expr { * @hideconstructor * @param value The value of the constant. */ - constructor(private value: unknown) { + constructor(private value: unknown, readonly _methodName: string | undefined) { super(); } @@ -2288,7 +2260,7 @@ export class Constant extends Expr { * @internal */ static _fromProto(value: ProtoValue): Constant { - const result = new Constant(value); + const result = new Constant(value, undefined); result._protoValue = value; return result; } @@ -2310,12 +2282,8 @@ export class Constant extends Expr { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, 'constant'); - + _readUserData(context: ParseContext): void { + context = this._methodName ? context.contextWith({methodName: this._methodName}) : context; if (isFirestoreValue(this._protoValue)) { return; } else { @@ -2415,10 +2383,21 @@ export function constant(value: ProtoValue): Constant; export function constant(value: VectorValue): Constant; export function constant(value: unknown): Constant { - return new Constant(value); + return _constant(value, 'contant'); +} + +/** + * @internal + * @private + * @param value + * @param methodName + */ +export function _constant(value: unknown, methodName: string | undefined): Constant { + return new Constant(value, methodName); } /** + * TODO remove * Creates a `Constant` instance for a VectorValue value. * * ```typescript @@ -2431,9 +2410,9 @@ export function constant(value: unknown): Constant { */ export function constantVector(value: number[] | VectorValue): Constant { if (value instanceof VectorValue) { - return new Constant(value); + return _constant(value, 'constantVector'); } else { - return new Constant(new VectorValue(value as number[])); + return _constant(new VectorValue(value as number[]), 'constantVector'); } } @@ -2443,20 +2422,16 @@ export function constantVector(value: number[] | VectorValue): Constant { * @private */ export class MapValue extends Expr { - constructor(private plainObject: Map) { + constructor(private plainObject: Map, readonly _methodName: string | undefined) { super(); } exprType: ExprType = 'Constant'; - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, '_map'); - + _readUserData(context: ParseContext): void { + context = this._methodName ? context.contextWith({methodName: this._methodName}) : context; this.plainObject.forEach(expr => { - expr._readUserData(dataReader, context); + expr._readUserData(context); }); } @@ -2477,7 +2452,9 @@ export class MapValue extends Expr { export class FunctionExpr extends Expr { readonly exprType: ExprType = 'Function'; - constructor(private name: string, private params: Expr[]) { + constructor( name: string, params: Expr[]); + constructor( name: string, params: Expr[], _methodName: string | undefined); + constructor(private name: string, private params: Expr[], readonly _methodName?: string) { super(); } @@ -2498,13 +2475,10 @@ export class FunctionExpr extends Expr { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, this.name); + _readUserData(context: ParseContext): void { + context = this._methodName ? context.contextWith({methodName: this._methodName}) : context; this.params.forEach(expr => { - return expr._readUserData(dataReader, context); + return expr._readUserData(context); }); } } @@ -2529,7 +2503,7 @@ export class BooleanExpr extends FunctionExpr { * @return A new `AggregateFunction` representing the 'countIf' aggregation. */ countIf(): AggregateFunction { - return new AggregateFunction('count_if', [this]); + return new AggregateFunction('count_if', [this], 'countIf'); } /** @@ -2543,7 +2517,7 @@ export class BooleanExpr extends FunctionExpr { * @return A new {@code Expr} representing the negated filter condition. */ not(): BooleanExpr { - return new BooleanExpr('not', [this]); + return new BooleanExpr('not', [this], 'not'); } } @@ -2572,7 +2546,7 @@ export function countIf(booleanExpr: BooleanExpr): AggregateFunction { * @returns A new `Expr` representing the 'rand' function. */ export function rand(): FunctionExpr { - return new FunctionExpr('rand', []); + return new FunctionExpr('rand', [], 'rand'); } /** @@ -2970,14 +2944,14 @@ export function bitRightShift( * * ```typescript * // Return the value in the tags field array at index 1. - * arrayOffset('tags', 1); + * arrayGet('tags', 1); * ``` * * @param arrayField The name of the array field. * @param offset The index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayOffset(arrayField: string, offset: number): FunctionExpr; +export function arrayGet(arrayField: string, offset: number): FunctionExpr; /** * @beta @@ -2988,14 +2962,14 @@ export function arrayOffset(arrayField: string, offset: number): FunctionExpr; * ```typescript * // Return the value in the tags field array at index specified by field * // 'favoriteTag'. - * arrayOffset('tags', field('favoriteTag')); + * arrayGet('tags', field('favoriteTag')); * ``` * * @param arrayField The name of the array field. * @param offsetExpr An Expr evaluating to the index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayOffset(arrayField: string, offsetExpr: Expr): FunctionExpr; +export function arrayGet(arrayField: string, offsetExpr: Expr): FunctionExpr; /** * @beta @@ -3005,14 +2979,14 @@ export function arrayOffset(arrayField: string, offsetExpr: Expr): FunctionExpr; * * ```typescript * // Return the value in the tags field array at index 1. - * arrayOffset(field('tags'), 1); + * arrayGet(field('tags'), 1); * ``` * * @param arrayExpression An Expr evaluating to an array. * @param offset The index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayOffset( +export function arrayGet( arrayExpression: Expr, offset: number ): FunctionExpr; @@ -3026,22 +3000,22 @@ export function arrayOffset( * ```typescript * // Return the value in the tags field array at index specified by field * // 'favoriteTag'. - * arrayOffset(field('tags'), field('favoriteTag')); + * arrayGet(field('tags'), field('favoriteTag')); * ``` * * @param arrayExpression An Expr evaluating to an array. * @param offsetExpr An Expr evaluating to the index of the element to return. - * @return A new Expr representing the 'arrayOffset' operation. + * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayOffset( +export function arrayGet( arrayExpression: Expr, offsetExpr: Expr ): FunctionExpr; -export function arrayOffset( +export function arrayGet( array: Expr | string, offset: Expr | number ): FunctionExpr { - return fieldOrExpression(array).arrayOffset(valueToDefaultExpr(offset)); + return fieldOrExpression(array).arrayGet(valueToDefaultExpr(offset)); } /** @@ -3070,7 +3044,7 @@ export function isError(value: Expr): BooleanExpr { * ```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")); + * ifError(field("title").arrayGet(0), field("title")); * ``` * * @param tryExpr The try expression. @@ -3089,7 +3063,7 @@ export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; * ```typescript * // Returns the first item in the title field arrays, or returns * // "Default Title" - * ifError(field("title").arrayOffset(0), "Default Title"); + * ifError(field("title").arrayGet(0), "Default Title"); * ``` * * @param tryExpr The try expression. @@ -3484,7 +3458,6 @@ export function substr( export function add( first: Expr, second: Expr | unknown, - ...others: Array ): FunctionExpr; /** @@ -3505,17 +3478,14 @@ export function add( export function add( fieldName: string, second: Expr | unknown, - ...others: Array ): FunctionExpr; export function add( first: Expr | string, second: Expr | unknown, - ...others: Array ): FunctionExpr { return fieldOrExpression(first).add( - valueToDefaultExpr(second), - ...others.map(value => valueToDefaultExpr(value)) + valueToDefaultExpr(second) ); } @@ -3609,7 +3579,6 @@ export function subtract( export function multiply( first: Expr, second: Expr | unknown, - ...others: Array ): FunctionExpr; /** @@ -3629,18 +3598,15 @@ export function multiply( */ export function multiply( fieldName: string, - second: Expr | unknown, - ...others: Array + second: Expr | unknown ): FunctionExpr; export function multiply( first: Expr | string, - second: Expr | unknown, - ...others: Array + second: Expr | unknown ): FunctionExpr { return fieldOrExpression(first).multiply( - valueToDefaultExpr(second), - ...others.map(valueToDefaultExpr) + valueToDefaultExpr(second) ); } @@ -3799,6 +3765,9 @@ export function mod(left: Expr | string, right: Expr | unknown): FunctionExpr { * @return A new {@code Expr} representing the map function. */ export function map(elements: Record): FunctionExpr { + return _map(elements, 'map'); +} +export function _map(elements: Record, methodName: string | undefined): FunctionExpr { const result: Expr[] = []; for (const key in elements) { if (Object.prototype.hasOwnProperty.call(elements, key)) { @@ -3807,7 +3776,7 @@ export function map(elements: Record): FunctionExpr { result.push(valueToDefaultExpr(value)); } } - return new FunctionExpr('map', result); + return new FunctionExpr('map', result, 'map'); } /** @@ -3829,7 +3798,7 @@ export function _mapValue(plainObject: Record): MapValue { result.set(key, valueToDefaultExpr(value)); } } - return new MapValue(result); + return new MapValue(result, undefined); } /** @@ -3846,9 +3815,13 @@ export function _mapValue(plainObject: Record): MapValue { * @return A new {@code Expr} representing the array function. */ export function array(elements: unknown[]): FunctionExpr { + return _array(elements, 'array'); +} +export function _array(elements: unknown[], methodName: string | undefined): FunctionExpr { return new FunctionExpr( 'array', - elements.map(element => valueToDefaultExpr(element)) + elements.map(element => valueToDefaultExpr(element)), + methodName ); } @@ -4781,7 +4754,7 @@ export function xor( second: BooleanExpr, ...additionalConditions: BooleanExpr[] ): BooleanExpr { - return new BooleanExpr('xor', [first, second, ...additionalConditions]); + return new BooleanExpr('xor', [first, second, ...additionalConditions], 'xor'); } /** @@ -4806,7 +4779,7 @@ export function cond( thenExpr: Expr, elseExpr: Expr ): FunctionExpr { - return new FunctionExpr('cond', [condition, thenExpr, elseExpr]); + return new FunctionExpr('cond', [condition, thenExpr, elseExpr], 'cond'); } /** @@ -5324,7 +5297,7 @@ export function like(stringExpression: Expr, pattern: Expr): BooleanExpr; export function like( left: Expr | string, pattern: Expr | string -): FunctionExpr { +): BooleanExpr { const leftExpr = fieldOrExpression(left); const patternExpr = valueToDefaultExpr(pattern); return leftExpr.like(patternExpr); @@ -5908,7 +5881,7 @@ export function mapGet( * @return A new {@code AggregateFunction} representing the 'countAll' aggregation. */ export function countAll(): AggregateFunction { - return new AggregateFunction('count', []); + return new AggregateFunction('count', [], 'count'); } /** @@ -6756,7 +6729,7 @@ export function and( second: BooleanExpr, ...more: BooleanExpr[] ): BooleanExpr { - return new BooleanExpr('and', [first, second, ...more]); + return new BooleanExpr('and', [first, second, ...more], 'and'); } /** @@ -6780,7 +6753,7 @@ export function or( second: BooleanExpr, ...more: BooleanExpr[] ): BooleanExpr { - return new BooleanExpr('or', [first, second, ...more]); + return new BooleanExpr('or', [first, second, ...more], 'xor'); } /** @@ -6815,7 +6788,7 @@ export function ascending(expr: Expr): Ordering; */ export function ascending(fieldName: string): Ordering; export function ascending(field: Expr | string): Ordering { - return new Ordering(fieldOrExpression(field), 'ascending'); + return new Ordering(fieldOrExpression(field), 'ascending', 'ascending'); } /** @@ -6850,7 +6823,7 @@ export function descending(expr: Expr): Ordering; */ export function descending(fieldName: string): Ordering; export function descending(field: Expr | string): Ordering { - return new Ordering(fieldOrExpression(field), 'descending'); + return new Ordering(fieldOrExpression(field), 'descending', 'descending'); } /** @@ -6863,7 +6836,8 @@ export function descending(field: Expr | string): Ordering { export class Ordering implements ProtoValueSerializable, UserData { constructor( readonly expr: Expr, - readonly direction: 'ascending' | 'descending' + readonly direction: 'ascending' | 'descending', + readonly _methodName: string | undefined ) {} /** @@ -6893,12 +6867,8 @@ export class Ordering implements ProtoValueSerializable, UserData { * @private * @internal */ - _readUserData(dataReader: UserDataReader, context?: ParseContext): void { - context = - this._createdFromLiteral && context - ? context - : dataReader.createContext(UserDataSource.Argument, 'constant'); - this.expr._readUserData(dataReader); + _readUserData(context: ParseContext): void { + this.expr._readUserData(context); } _protoValueType: 'ProtoValue' = 'ProtoValue'; diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts index 421fc759bfb..de28ceecca6 100644 --- a/packages/firestore/src/lite-api/pipeline-source.ts +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -15,12 +15,13 @@ * limitations under the License. */ -import { DatabaseId } from '../core/database_info'; -import { toPipeline } from '../core/pipeline-util'; -import { FirestoreError, Code } from '../util/error'; +import {DatabaseId} from '../core/database_info'; +import {toPipeline} from '../core/pipeline-util'; +import {Code, FirestoreError} from '../util/error'; +import {isCollectionReference, isString} from "../util/types"; -import { Pipeline } from './pipeline'; -import { CollectionReference, DocumentReference, Query } from './reference'; +import {Pipeline} from './pipeline'; +import {CollectionReference, DocumentReference, Query} from './reference'; import { CollectionGroupSource, CollectionSource, @@ -28,6 +29,13 @@ import { DocumentsSource, Stage } from './stage'; +import { + CollectionGroupStageOptions, + CollectionStageOptions, + DatabaseStageOptions, + DocumentsStageOptions +} from "./stage_options"; +import {UserDataReader, UserDataSource} from "./user_data_reader"; /** * Represents the source of a Firestore {@link Pipeline}. @@ -37,10 +45,13 @@ export class PipelineSource { /** * @internal * @private + * @param databaseId + * @param userDataReader * @param _createPipeline */ constructor( private databaseId: DatabaseId, + private userDataReader: UserDataReader, /** * @internal * @private @@ -49,44 +60,123 @@ export class PipelineSource { ) {} /** - * 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. + * Returns all documents from the entire collection. The collection can be nested. + * @param collection - Name or reference to the collection that will be used as the Pipeline source. */ - collection(collectionPath: string): PipelineType; - + collection(collection: string | CollectionReference): 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. + * Returns all documents from the entire collection. The collection can be nested. + * @param options - Options defining how this CollectionStage is evaluated. */ - 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)]); + collection(options: CollectionStageOptions): PipelineType; + collection( + collectionOrOptions: + | string + | CollectionReference + | CollectionStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + const options = + isString(collectionOrOptions) || + isCollectionReference(collectionOrOptions) + ? {} + : collectionOrOptions; + const collectionRefOrString = + isString(collectionOrOptions) || + isCollectionReference(collectionOrOptions) + ? collectionOrOptions + : collectionOrOptions.collection; + + // Validate that a user provided reference is for the same Firestore DB + if (isCollectionReference(collectionRefOrString)) { + this._validateReference(collectionRefOrString); } + + // Convert user land convenience types to internal types + const normalizedCollection = isString(collectionRefOrString) + ? (collectionRefOrString as string) + : collectionRefOrString.path; + + // Create stage object + const stage = new CollectionSource( + normalizedCollection, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'collection'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); } /** - * 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. + * Returns all documents from a collection ID regardless of the parent. + * @param collectionId - ID of the collection group to use as the Pipeline source. */ - collectionGroup(collectionId: string): PipelineType { - return this._createPipeline([new CollectionGroupSource(collectionId)]); + collectionGroup(collectionId: string): PipelineType; + /** + * Returns all documents from a collection ID regardless of the parent. + * @param options - Options defining how this CollectionGroupStage is evaluated. + */ + collectionGroup( + options: CollectionGroupStageOptions + ): PipelineType; + collectionGroup( + collectionIdOrOptions: + | string + | CollectionGroupStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + let collectionId: string; + let options: {}; + if (isString( + collectionIdOrOptions + )) { + collectionId = collectionIdOrOptions; + options = {}; + } else { + ({collectionId, ...options} = collectionIdOrOptions); + } + + // Create stage object + const stage = new CollectionGroupSource( + collectionId, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'collectionGroup'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); } /** - * Set the pipeline's source to be all documents in this database. + * Returns all documents from the entire database. */ - database(): PipelineType { - return this._createPipeline([new DatabaseSource()]); + database(): PipelineType; + /** + * Returns all documents from the entire database. + * @param options - Options defining how a DatabaseStage is evaluated. + */ + database(options: DatabaseStageOptions): PipelineType; + database(options?: DatabaseStageOptions): PipelineType { + // Process argument union(s) from method overloads + options = options ?? {}; + + // Create stage object + const stage = new DatabaseSource(options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'database'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); } /** @@ -97,14 +187,55 @@ export class PipelineSource { * * @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)]); + documents(docs: Array): PipelineType; + + /** + * Set the pipeline's source to the documents specified by the given paths and DocumentReferences. + * + * @param options - Options defining how this DocumentsStage is evaluated. + * + * @throws {@FirestoreError} Thrown if any of the provided DocumentReferences target a different project or database than the pipeline. + */ + documents(options: DocumentsStageOptions): PipelineType; + documents( + docsOrOptions: + | Array + | DocumentsStageOptions + ): PipelineType { + // Process argument union(s) from method overloads + let options: {}; + let docs: Array; + if (Array.isArray(docsOrOptions)) { + docs = docsOrOptions; + options = {}; + } else { + ({docs, ...options} = docsOrOptions); + } + + // Validate that all user provided references are for the same Firestore DB + docs + .filter(v => v instanceof DocumentReference) + .forEach(dr => + this._validateReference(dr as DocumentReference) + ); + + // Convert user land convenience types to internal types + const normalizedDocs: string[] = docs.map(doc => + isString(doc) ? doc : doc.path + ); + + // Create stage object + const stage = new DocumentsSource( + normalizedDocs, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'documents'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._createPipeline([stage]); } /** diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts index 8b5bebeddb8..4f2522320a6 100644 --- a/packages/firestore/src/lite-api/pipeline.ts +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -15,61 +15,75 @@ * limitations under the License. */ -import { ObjectValue } from '../model/object_value'; +import { isNumber } from 'util'; + import { Pipeline as ProtoPipeline, Stage as ProtoStage } from '../protos/firestore_proto_api'; -import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; -import { isPlainObject } from '../util/input_validation'; +import {JsonProtoSerializer, ProtoSerializable} from '../remote/serializer'; +import {isPlainObject} from '../util/input_validation'; +import { + aliasedAggregateToMap, fieldOrExpression, + selectablesToMap, + vectorToExpr +} from "../util/pipeline_util"; +import { + isAliasedAggregate, + isBooleanExpr, isExpr, + isField, isLitePipeline, isOrdering, + isSelectable, + isString, toField +} from "../util/types"; -import { Firestore } from './database'; +import {Firestore} from './database'; import { _mapValue, AggregateFunction, AggregateWithAlias, + BooleanExpr, _constant, Expr, - ExprWithAlias, Field, - BooleanExpr, - Ordering, - Selectable, field, - Constant + Ordering, + Selectable, _field } from './expressions'; import { AddFields, Aggregate, Distinct, FindNearest, - FindNearestOptions, GenericStage, Limit, Offset, RemoveFields, Replace, + Sample, Select, Sort, - Sample, + Stage, Union, Unnest, - Stage, Where } from './stage'; import { - parseVectorValue, + AddFieldsStageOptions, + AggregateStageOptions, + DistinctStageOptions, + FindNearestStageOptions, + LimitStageOptions, + OffsetStageOptions, + RemoveFieldsStageOptions, ReplaceWithStageOptions, SampleStageOptions, + SelectStageOptions, + SortStageOptions, StageOptions, UnionStageOptions, UnnestStageOptions, + WhereStageOptions +} from "./stage_options"; +import { UserDataReader, UserDataSource } from './user_data_reader'; -import { AbstractUserDataWriter } from './user_data_writer'; +import {AbstractUserDataWriter} from './user_data_writer'; -interface ReadableUserData { - _readUserData(dataReader: UserDataReader): void; -} - -function isReadableUserData(value: unknown): value is ReadableUserData { - return typeof (value as ReadableUserData)._readUserData === 'function'; -} /** * @beta @@ -165,15 +179,68 @@ export class Pipeline implements ProtoSerializable { * @param additionalFields Optional additional fields to add to the documents, specified as {@link Selectable}s. * @return A new Pipeline object with this stage appended to the stage list. */ - addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline { - return this._addStage( - new AddFields( - this.readUserData( - 'addFields', - this.selectablesToMap([field, ...additionalFields]) - ) - ) - ); + addFields( + field: Selectable, + ...additionalFields: Selectable[] + ): Pipeline; + /** + * Adds new fields to outputs from previous stages. + * + * This stage allows you to compute values on-the-fly based on existing data from previous + * stages or constants. You can use this to create new fields or overwrite existing ones (if there + * is name overlaps). + * + * The added fields are defined using {@link Selectable}s, which can be: + * + * - {@link Field}: References an existing document field. + * - {@link Expr}: Either a literal value (see {@link Constant}) or a computed value + * (see {@FunctionExpr}) with an assigned alias using {@link Expr#as}. + * + * Example: + * + * ```typescript + * firestore.pipeline().collection("books") + * .addFields( + * field("rating").as("bookRating"), // Rename 'rating' to 'bookRating' + * add(5, field("quantity")).as("totalCost") // Calculate 'totalCost' + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + addFields(options: AddFieldsStageOptions): Pipeline; + addFields( + fieldOrOptions: + | Selectable + | AddFieldsStageOptions, + ...additionalFields: Selectable[] + ): Pipeline { + // Process argument union(s) from method overloads + let fields: Selectable[]; + let options: {}; + if (isSelectable(fieldOrOptions)) { + fields = [fieldOrOptions, ...additionalFields]; + options = {}; + } else { + ({fields, ...options} = fieldOrOptions); + } + + // Convert user land convenience types to internal types + const normalizedFields: Map = selectablesToMap(fields); + + // Create stage object + const stage = new AddFields( + normalizedFields, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'addFields'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -197,12 +264,57 @@ export class Pipeline implements ProtoSerializable { removeFields( fieldValue: Field | string, ...additionalFields: Array + ): Pipeline; + /** + * Remove fields from outputs of previous stages. + * + * Example: + * + * ```typescript + * firestore.pipeline().collection('books') + * // removes field 'rating' and 'cost' from the previous stage outputs. + * .removeFields( + * field('rating'), + * 'cost' + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + removeFields(options: RemoveFieldsStageOptions): Pipeline; + removeFields( + fieldValueOrOptions: + | Field + | string + | RemoveFieldsStageOptions, + ...additionalFields: Array ): Pipeline { - const fieldExpressions = [fieldValue, ...additionalFields].map(f => - typeof f === 'string' ? field(f) : (f as Field) + // Process argument union(s) from method overloads + const options = + isField(fieldValueOrOptions) || isString(fieldValueOrOptions) + ? {} + : fieldValueOrOptions; + const fields: Array = + isField(fieldValueOrOptions) || isString(fieldValueOrOptions) + ? [fieldValueOrOptions, ...additionalFields] + : fieldValueOrOptions.fields; + + // Convert user land convenience types to internal types + const convertedFields: Field[] = fields.map(f => + isString(f) ? field(f) : (f as Field) ); - this.readUserData('removeFields', fieldExpressions); - return this._addStage(new RemoveFields(fieldExpressions)); + + // Create stage object + const stage = new RemoveFields(convertedFields, options); + + // User data must be read in the context of the API method to + // provide contextual errors + stage._readUserData( this.userDataReader.createContext(UserDataSource.Argument, 'removeFields')); + + // Add stage to the pipeline + return this._addStage(stage); + } /** @@ -224,7 +336,7 @@ export class Pipeline implements ProtoSerializable { *

Example: * * ```typescript - * firestore.pipeline().collection("books") + * db.pipeline().collection("books") * .select( * "firstName", * field("lastName"), @@ -241,13 +353,70 @@ export class Pipeline implements ProtoSerializable { select( selection: Selectable | string, ...additionalSelections: Array + ): Pipeline; + /** + * Selects or creates a set of fields from the outputs of previous stages. + * + *

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

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

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

Example: + * + * ```typescript + * db.pipeline().collection("books") + * .select( + * "firstName", + * field("lastName"), + * field("address").toUppercase().as("upperAddress"), + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + select(options: SelectStageOptions): Pipeline; + select( + selectionOrOptions: + | Selectable + | string + | SelectStageOptions, + ...additionalSelections: Array ): Pipeline { - let projections: Map = this.selectablesToMap([ - selection, - ...additionalSelections - ]); - projections = this.readUserData('select', projections); - return this._addStage(new Select(projections)); + // Process argument union(s) from method overloads + const options = + isSelectable(selectionOrOptions) || isString(selectionOrOptions) + ? {} + : selectionOrOptions; + + const selections: Array = + isSelectable(selectionOrOptions) || isString(selectionOrOptions) + ? [selectionOrOptions, ...additionalSelections] + : selectionOrOptions.selections; + + // Convert user land convenience types to internal types + const normalizedSelections: Map = + selectablesToMap(selections); + + // Create stage object + const stage = new Select(normalizedSelections, options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'select'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -281,9 +450,64 @@ export class Pipeline implements ProtoSerializable { * @param condition The {@link BooleanExpr} to apply. * @return A new Pipeline object with this stage appended to the stage list. */ - where(condition: BooleanExpr): Pipeline { - this.readUserData('where', condition); - return this._addStage(new Where(condition)); + where(condition: BooleanExpr): Pipeline; + /** + * Filters the documents from previous stages to only include those matching the specified {@link + * BooleanExpr}. + * + *

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

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

Example: + * + * ```typescript + * firestore.pipeline().collection("books") + * .where( + * and( + * gt(field("rating"), 4.0), // Filter for ratings greater than 4.0 + * field("genre").eq("Science Fiction") // Equivalent to gt("genre", "Science Fiction") + * ) + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + where(options: WhereStageOptions): Pipeline; + where( + conditionOrOptions: + | BooleanExpr + | WhereStageOptions + ): Pipeline { + // Process argument union(s) from method overloads + const options = isBooleanExpr(conditionOrOptions) ? {} : conditionOrOptions; + const condition: BooleanExpr = isBooleanExpr( + conditionOrOptions + ) + ? conditionOrOptions + : conditionOrOptions.condition; + + // Create stage object + const stage = new Where( + condition, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'where'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -297,8 +521,8 @@ export class Pipeline implements ProtoSerializable { * * ```typescript * // Retrieve the second page of 20 results - * firestore.pipeline().collection("books") - * .sort(field("published").descending()) + * firestore.pipeline().collection('books') + * .sort(field('published').descending()) * .offset(20) // Skip the first 20 results * .limit(20); // Take the next 20 results * ``` @@ -306,8 +530,49 @@ export class Pipeline implements ProtoSerializable { * @param offset The number of documents to skip. * @return A new Pipeline object with this stage appended to the stage list. */ - offset(offset: number): Pipeline { - return this._addStage(new Offset(offset)); + offset(offset: number): Pipeline; + /** + * Skips the first `offset` number of documents from the results of previous stages. + * + *

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

Example: + * + * ```typescript + * // Retrieve the second page of 20 results + * firestore.pipeline().collection('books') + * .sort(field('published').descending()) + * .offset(20) // Skip the first 20 results + * .limit(20); // Take the next 20 results + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + offset(options: OffsetStageOptions): Pipeline; + offset( + offsetOrOptions: number | OffsetStageOptions + ): Pipeline { + // Process argument union(s) from method overloads + const options = isNumber(offsetOrOptions) ? {} : offsetOrOptions; + const offset: number = isNumber(offsetOrOptions) + ? offsetOrOptions + : offsetOrOptions.offset; + + // Create stage object + const stage = new Offset( + offset, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'offset'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -327,16 +592,62 @@ export class Pipeline implements ProtoSerializable { * * ```typescript * // Limit the results to the top 10 highest-rated books - * firestore.pipeline().collection("books") - * .sort(field("rating").descending()) + * firestore.pipeline().collection('books') + * .sort(field('rating').descending()) * .limit(10); * ``` * * @param limit The maximum number of documents to return. * @return A new Pipeline object with this stage appended to the stage list. */ - limit(limit: number): Pipeline { - return this._addStage(new Limit(limit)); + limit(limit: number): Pipeline; + /** + * Limits the maximum number of documents returned by previous stages to `limit`. + * + *

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

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

Example: + * + * ```typescript + * // Limit the results to the top 10 highest-rated books + * firestore.pipeline().collection('books') + * .sort(field('rating').descending()) + * .limit(10); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new Pipeline object with this stage appended to the stage list. + */ + limit(options: LimitStageOptions): Pipeline; + limit( + limitOrOptions: number | LimitStageOptions + ): Pipeline { + // Process argument union(s) from method overloads + const options = isNumber(limitOrOptions) ? {} : limitOrOptions; + const limit: number = isNumber(limitOrOptions) + ? limitOrOptions + : limitOrOptions.limit; + + // Create stage object + const stage = new Limit( + limit, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'limit'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -349,7 +660,7 @@ export class Pipeline implements ProtoSerializable { * * - {@code string}: Name of an existing field * - {@link Field}: References an existing document field. - * - {@link ExprWithAlias}: Represents the result of a function with an assigned alias name + * - {@link AliasedExpr}: Represents the result of a function with an assigned alias name * using {@link Expr#as}. * * Example: @@ -370,15 +681,65 @@ export class Pipeline implements ProtoSerializable { distinct( group: string | Selectable, ...additionalGroups: Array + ): Pipeline; + /** + * Returns a set of distinct values from the inputs to this stage. + * + * This stage runs through the results from previous stages to include only results with + * unique combinations of {@link Expr} values ({@link Field}, {@link Function}, etc). + * + * The parameters to this stage are defined using {@link Selectable} expressions or strings: + * + * - {@code string}: Name of an existing field + * - {@link Field}: References an existing document field. + * - {@link AliasedExpr}: Represents the result of a function with an assigned alias name + * using {@link Expr#as}. + * + * Example: + * + * ```typescript + * // Get a list of unique author names in uppercase and genre combinations. + * firestore.pipeline().collection("books") + * .distinct(toUppercase(field("author")).as("authorName"), field("genre"), "publishedAt") + * .select("authorName"); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + distinct(options: DistinctStageOptions): Pipeline; + distinct( + groupOrOptions: + | string + | Selectable + | DistinctStageOptions, + ...additionalGroups: Array ): Pipeline { - return this._addStage( - new Distinct( - this.readUserData( - 'distinct', - this.selectablesToMap([group, ...additionalGroups]) - ) - ) - ); + // Process argument union(s) from method overloads + const options = + isString(groupOrOptions) || isSelectable(groupOrOptions) + ? {} + : groupOrOptions; + const groups: Array = + isString(groupOrOptions) || isSelectable(groupOrOptions) + ? [groupOrOptions, ...additionalGroups] + : groupOrOptions.groups; + + // Convert user land convenience types to internal types + const convertedGroups: Map = selectablesToMap(groups); + + // Create stage object + const stage = new Distinct( + convertedGroups, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'distinct'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -437,79 +798,98 @@ export class Pipeline implements ProtoSerializable { * }); * ``` * - * @param options An object that specifies the accumulators - * and optional grouping fields to perform. + * @param options - An object that specifies required and optional parameters for the stage. * @return A new {@code Pipeline} object with this stage appended to the stage * list. */ - aggregate(options: { - accumulators: AggregateWithAlias[]; - groups?: Array; - }): Pipeline; + aggregate(options: AggregateStageOptions): Pipeline; aggregate( - optionsOrTarget: + targetOrOptions: | AggregateWithAlias - | { - accumulators: AggregateWithAlias[]; - groups?: Array; - }, + | AggregateStageOptions, ...rest: AggregateWithAlias[] ): Pipeline { - if ('accumulators' in optionsOrTarget) { - return this._addStage( - new Aggregate( - new Map( - optionsOrTarget.accumulators.map((target: AggregateWithAlias) => { - this.readUserData( - 'aggregate', - target as unknown as AggregateWithAlias - ); - return [ - (target as unknown as AggregateWithAlias).alias, - (target as unknown as AggregateWithAlias).aggregate - ]; - }) - ), - this.readUserData( - 'aggregate', - this.selectablesToMap(optionsOrTarget.groups || []) - ) - ) - ); - } else { - return this._addStage( - new Aggregate( - new Map( - [optionsOrTarget, ...rest].map(target => [ - (target as unknown as AggregateWithAlias).alias, - this.readUserData( - 'aggregate', - (target as unknown as AggregateWithAlias).aggregate - ) - ]) - ), - new Map() - ) - ); - } + // Process argument union(s) from method overloads + const options = isAliasedAggregate(targetOrOptions) ? {} : targetOrOptions; + const accumulators: AggregateWithAlias[] = + isAliasedAggregate(targetOrOptions) + ? [targetOrOptions, ...rest] + : targetOrOptions.accumulators; + const groups: Array = + isAliasedAggregate(targetOrOptions) ? [] : targetOrOptions.groups ?? []; + + // Convert user land convenience types to internal types + const convertedAccumulators: Map = + aliasedAggregateToMap(accumulators); + const convertedGroups: Map = selectablesToMap(groups); + + // Create stage object + const stage = new Aggregate( + convertedGroups, + convertedAccumulators, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'aggregate'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } - findNearest(options: FindNearestOptions): Pipeline { - const parseContext = this.userDataReader.createContext( - UserDataSource.Argument, - 'findNearest' - ); - const value = parseVectorValue(options.vectorValue, parseContext); - const vectorObjectValue = new ObjectValue(value); - return this._addStage( - new FindNearest( - options.field instanceof Field ? options.field : field(options.field), - vectorObjectValue, - options.distanceMeasure, - options.limit, - options.distanceField - ) - ); + /** + * Performs a vector proximity search on the documents from the previous stage, returning the + * K-nearest documents based on the specified query `vectorValue` and `distanceMeasure`. The + * returned documents will be sorted in order from nearest to furthest from the query `vectorValue`. + * + *

Example: + * + * ```typescript + * // Find the 10 most similar books based on the book description. + * const bookDescription = "Lorem ipsum..."; + * const queryVector: number[] = ...; // compute embedding of `bookDescription` + * + * firestore.pipeline().collection("books") + * .findNearest({ + * field: 'embedding', + * vectorValue: queryVector, + * distanceMeasure: 'euclidean', + * limit: 10, // optional + * distanceField: 'computedDistance' // optional + * }); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + findNearest(options: FindNearestStageOptions): Pipeline { + // Convert user land convenience types to internal types + const field = toField(options.field); + const vectorValue = vectorToExpr(options.vectorValue); + const distanceField = options.distanceField + ? toField(options.distanceField) + : undefined; + const internalOptions = { + distanceField, + limit: options.limit, + rawOptions: options.rawOptions + }; + + // Create stage object + const stage = new FindNearest( + vectorValue, + field, + options.distanceMeasure, + internalOptions); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'addFields'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -537,11 +917,61 @@ export class Pipeline implements ProtoSerializable { * @param additionalOrderings Optional additional {@link Ordering} instances specifying the additional sorting criteria. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline { - // Ordering object - return this._addStage( - new Sort(this.readUserData('sort', [ordering, ...additionalOrderings])) - ); + sort( + ordering: Ordering, + ...additionalOrderings: Ordering[] + ): Pipeline; + /** + * Sorts the documents from previous stages based on one or more {@link Ordering} criteria. + * + *

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

Example: + * + * ```typescript + * // Sort books by rating in descending order, and then by title in ascending order for books + * // with the same rating + * firestore.pipeline().collection("books") + * .sort( + * Ordering.of(field("rating")).descending(), + * Ordering.of(field("title")) // Ascending order is the default + * ); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + sort(options: SortStageOptions): Pipeline; + sort( + orderingOrOptions: + | Ordering + | SortStageOptions, + ...additionalOrderings: Ordering[] + ): Pipeline { + // Process argument union(s) from method overloads + const options = isOrdering(orderingOrOptions) ? {} : orderingOrOptions; + const orderings: Ordering[] = isOrdering( + orderingOrOptions + ) + ? [orderingOrOptions, ...additionalOrderings] + : orderingOrOptions.orderings; + + // Create stage object + const stage = new Sort( + orderings, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'sort'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -576,7 +1006,6 @@ export class Pipeline implements ProtoSerializable { * @return A new {@code Pipeline} object with this stage appended to the stage list. */ replaceWith(fieldName: string): Pipeline; - /** * Fully overwrites all fields in a document with those coming from a map. * @@ -614,10 +1043,72 @@ export class Pipeline implements ProtoSerializable { * @return A new {@code Pipeline} object with this stage appended to the stage list. */ replaceWith(expr: Expr): Pipeline; - replaceWith(value: Expr | string): Pipeline { - const fieldExpr = typeof value === 'string' ? field(value) : value; - this.readUserData('replaceWith', fieldExpr); - return this._addStage(new Replace(fieldExpr, 'full_replace')); + /** + * Fully overwrites all fields in a document with those coming from a map. + * + *

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

Example: + * + * ```typescript + * // Input. + * // { + * // 'name': 'John Doe Jr.', + * // 'parents': { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * // } + * + * // Emit parents as document. + * firestore.pipeline().collection('people').replaceWith(map({ + * foo: 'bar', + * info: { + * name: field('name') + * } + * })); + * + * // Output + * // { + * // 'father': 'John Doe Sr.', + * // 'mother': 'Jane Doe' + * // } + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + replaceWith(options: ReplaceWithStageOptions): Pipeline; + replaceWith( + valueOrOptions: + | Expr + | string + | ReplaceWithStageOptions + ): Pipeline { + // Process argument union(s) from method overloads + const options = + isString(valueOrOptions) || isExpr(valueOrOptions) ? {} : valueOrOptions; + const fieldNameOrExpr: string | Expr = + isString(valueOrOptions) || isExpr(valueOrOptions) + ? valueOrOptions + : valueOrOptions.map; + + // Convert user land convenience types to internal types + const mapExpr = fieldOrExpression(fieldNameOrExpr); + + // Create stage object + const stage = new Replace( + mapExpr, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'replaceWith'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -655,24 +1146,41 @@ export class Pipeline implements ProtoSerializable { * firestore.pipeline().collection("books") * .sample({ percentage: 0.5 }); * - * @param options The {@code SampleOptions} specifies how sampling is performed. + * @param options - An object that specifies required and optional parameters for the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - sample(options: { percentage: number } | { documents: number }): Pipeline; + sample(options: SampleStageOptions): Pipeline; sample( - documentsOrOptions: number | { percentage: number } | { documents: number } + documentsOrOptions: number | SampleStageOptions ): 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') - ); + // Process argument union(s) from method overloads + const options = isNumber(documentsOrOptions) ? {} : documentsOrOptions; + let rate: number; + let mode: 'documents' | 'percent'; + if (isNumber(documentsOrOptions)) { + rate = documentsOrOptions; + mode = 'documents'; + } else if (isNumber(documentsOrOptions.documents)) { + rate = documentsOrOptions.documents; + mode = 'documents'; } else { - return this._addStage( - new Sample(documentsOrOptions.documents, 'documents') - ); + rate = documentsOrOptions.percentage!; + mode = 'percent'; } + + // Create stage object + const stage = new Sample( + rate, + mode, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'sample'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -693,10 +1201,89 @@ export class Pipeline implements ProtoSerializable { * @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)); + union(other: Pipeline): Pipeline; + /** + * Performs union of all documents from two pipelines, including duplicates. + * + *

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

Example: + * + * ```typescript + * // Emit documents from books collection and magazines collection. + * firestore.pipeline().collection('books') + * .union(firestore.pipeline().collection('magazines')); + * ``` + * + * @param options - An object that specifies required and optional parameters for the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + union(options: UnionStageOptions): Pipeline; + union( + otherOrOptions: + | Pipeline + | UnionStageOptions + ): Pipeline { + // Process argument union(s) from method overloads + let options: {}; + let otherPipeline: Pipeline; + if (isLitePipeline(otherOrOptions)) { + options = {}; + otherPipeline = otherOrOptions; + } else { + ({other: otherPipeline, ...options} = otherOrOptions); + } + + // Create stage object + const stage = new Union( + otherPipeline, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'union'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } + /** + * Produces a document for each element in an input array. + * + * For each previous stage document, this stage will emit zero or more augmented documents. The + * input array specified by the `selectable` parameter, will emit an augmented document for each input array element. The input array element will + * augment the previous stage document by setting the `alias` field with the array element value. + * + * When `selectable` evaluates to a non-array value (ex: number, null, absent), then the stage becomes a no-op for + * the current input document, returning it as is with the `alias` field absent. + * + * No documents are emitted when `selectable` evaluates to an empty array. + * + * Example: + * + * ```typescript + * // Input: + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space", "adventure" ], ... } + * + * // Emit a book document for each tag of the book. + * firestore.pipeline().collection("books") + * .unnest(field("tags").as('tag')); + * + * // Output: + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", ... } + * ``` + * + * @param selectable A selectable expression defining the field to unnest and the alias to use for each un-nested element in the output documents. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + unnest( + selectable: Selectable + ): Pipeline; /** * Produces a document for each element in an input array. * @@ -725,50 +1312,74 @@ export class Pipeline implements ProtoSerializable { * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", "tagIndex": 2, ... } * ``` * - * @param selectable A selectable expression defining the field to unnest and the alias to use for each un-nested element in the output documents. - * @param indexField An optional string value specifying the field path to write the offset (starting at zero) into the array the un-nested element is from + * @param options - An object that specifies required and optional parameters for the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - unnest(selectable: Selectable, indexField?: string): Pipeline { - this.readUserData('unnest', selectable.expr); - - const alias = field(selectable.alias); - this.readUserData('unnest', alias); - - if (indexField) { - return this._addStage( - new Unnest(selectable.expr, alias, field(indexField)) - ); + unnest(options: UnnestStageOptions): Pipeline; + unnest( + selectableOrOptions: + | Selectable + | UnnestStageOptions + ): Pipeline { + // Process argument union(s) from method overloads + let options: {indexField?: string | Field} & StageOptions; + let selectable: Selectable; + if (isSelectable(selectableOrOptions)) { + options = {}; + selectable = selectableOrOptions; } else { - return this._addStage(new Unnest(selectable.expr, alias)); + ({selectable, ...options} = selectableOrOptions); + } + + // Convert user land convenience types to internal types + const alias = selectable.alias; + const expr = selectable.expr as Expr; + if (isString(options['indexField'])) { + options.indexField = _field(options.indexField, 'unnest'); } + + // Create stage object + const stage = new Unnest( + alias, + expr, + options); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'unnest'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** - * Adds a generic stage to the pipeline. + * Adds a raw stage to the pipeline. * *

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

Example (Assuming there is no "where" stage available in SDK): + *

Example (Assuming there is no 'where' stage available in SDK): * * ```typescript - * // Assume we don't have a built-in "where" stage - * firestore.pipeline().collection("books") - * .genericStage("where", [field("published").lt(1900)]) // Custom "where" stage - * .select("title", "author"); + * // Assume we don't have a built-in 'where' stage + * firestore.pipeline().collection('books') + * .rawStage('where', [field('published').lt(1900)]) // Custom 'where' stage + * .select('title', 'author'); * ``` * - * @param name The unique name of the generic stage to add. - * @param params A list of parameters to configure the generic stage's behavior. + * @param name - The unique name of the raw stage to add. + * @param params - A list of parameters to configure the raw stage's behavior. + * @param options - An object of key value pairs that specifies optional parameters for the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - genericStage(name: string, params: unknown[]): Pipeline { - // Convert input values to Expressions. - // We treat objects as mapValues and arrays as arrayValues, - // this is unlike the default conversion for objects and arrays - // passed to an expression. + rawStage( + name: string, + params: unknown[], + options?: {[key: string]: Expr | unknown} + ): Pipeline { + // Convert user land convenience types to internal types const expressionParams = params.map((value: unknown) => { if (value instanceof Expr) { return value; @@ -777,16 +1388,23 @@ export class Pipeline implements ProtoSerializable { } else if (isPlainObject(value)) { return _mapValue(value as Record); } else { - return new Constant(value); + return _constant(value, 'rawStage'); } }); - expressionParams.forEach(param => { - if (isReadableUserData(param)) { - param._readUserData(this.userDataReader); - } - }); - return this._addStage(new GenericStage(name, expressionParams)); + // Create stage object + const stage = new GenericStage( + name, + expressionParams, + options ?? {}); + + // User data must be read in the context of the API method to + // provide contextual errors + const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'rawStage'); + stage._readUserData(parseContext); + + // Add stage to the pipeline + return this._addStage(stage); } /** @@ -811,47 +1429,6 @@ 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.alias, selectable.expr); - } else if (selectable instanceof ExprWithAlias) { - result.set(selectable.alias, selectable.expr); - } - } - return result; - } - - /** - * Reads user data for each expression in the expressionMap. - * @param name Name of the calling function. Used for error messages when invalid user data is encountered. - * @param expressionMap - * @return the expressionMap argument. - * @private - */ - private readUserData< - T extends - | Map - | ReadableUserData[] - | ReadableUserData - >(name: string, expressionMap: T): T { - if (isReadableUserData(expressionMap)) { - expressionMap._readUserData(this.userDataReader); - } else if (Array.isArray(expressionMap)) { - expressionMap.forEach(readableData => - readableData._readUserData(this.userDataReader) - ); - } else { - expressionMap.forEach(expr => expr._readUserData(this.userDataReader)); - } - return expressionMap; - } - /** * @internal * @private diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts index 530c9a61975..1d8e0297122 100644 --- a/packages/firestore/src/lite-api/pipeline_impl.ts +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -85,7 +85,7 @@ export function execute(pipeline: Pipeline): Promise { const context = udr.createContext(UserDataSource.Argument, 'execute'); const structuredPipelineOptions = new StructuredPipelineOptions({}, {}); - structuredPipelineOptions._readUserData(udr, context); + structuredPipelineOptions._readUserData(context); const structuredPipeline: StructuredPipeline = new StructuredPipeline( pipeline, @@ -123,7 +123,7 @@ export function execute(pipeline: Pipeline): Promise { Firestore.prototype.pipeline = function (): PipelineSource { const userDataWriter = new LiteUserDataWriter(this); const userDataReader = newUserDataReader(this); - return new PipelineSource(this._databaseId, (stages: Stage[]) => { + return new PipelineSource(this._databaseId, userDataReader, (stages: Stage[]) => { return new Pipeline(this, userDataReader, userDataWriter, stages); }); }; diff --git a/packages/firestore/src/lite-api/pipeline_settings.ts b/packages/firestore/src/lite-api/pipeline_options.ts similarity index 100% rename from packages/firestore/src/lite-api/pipeline_settings.ts rename to packages/firestore/src/lite-api/pipeline_options.ts diff --git a/packages/firestore/src/lite-api/stage.ts b/packages/firestore/src/lite-api/stage.ts index 31faaa00e76..35376f07d4b 100644 --- a/packages/firestore/src/lite-api/stage.ts +++ b/packages/firestore/src/lite-api/stage.ts @@ -15,67 +15,113 @@ * limitations under the License. */ -import { ObjectValue } from '../model/object_value'; +import {ParseContext} from "../api/parse_context"; +import { OptionsUtil} from "../core/options_util"; import { - Stage as ProtoStage, - Value as ProtoValue + ApiClientObjectMap, firestoreV1ApiClientInterfaces, + Stage as ProtoStage } from '../protos/firestore_proto_api'; -import { toNumber } from '../remote/number_serializer'; +import {toNumber} from '../remote/number_serializer'; import { JsonProtoSerializer, ProtoSerializable, toMapValue, toPipelineValue, - toStringValue + toStringValue, } from '../remote/serializer'; -import { hardAssert } from '../util/assert'; +import {hardAssert} from '../util/assert'; import { AggregateFunction, + BooleanExpr, Expr, Field, - BooleanExpr, - Ordering, - field + field, + Ordering } from './expressions'; -import { Pipeline } from './pipeline'; -import { DocumentReference } from './reference'; -import { VectorValue } from './vector_value'; +import {Pipeline} from './pipeline'; +import { StageOptions +} from "./stage_options"; +import { + isUserData, + UserData +} from "./user_data_reader"; + + +import Value = firestoreV1ApiClientInterfaces.Value; /** * @beta */ -export interface Stage extends ProtoSerializable { - name: string; +export abstract class Stage implements ProtoSerializable, UserData { + /** + * Store optionsProto parsed by _readUserData. + * @private + * @internal + * @protected + */ + protected optionsProto: ApiClientObjectMap | undefined = undefined; + protected knownOptions: Record; + protected rawOptions?: Record; + + constructor(options: StageOptions) { + ({rawOptions: this.rawOptions, ...this.knownOptions} = options); + } + + _readUserData(context: ParseContext): void { + this.optionsProto = this._optionsUtil.getOptionsProto(context, this.knownOptions, this.rawOptions); + } + + _toProto(_: JsonProtoSerializer): ProtoStage { + return { + name: this._name, + options: this.optionsProto + }; + } + + abstract get _optionsUtil(): OptionsUtil; + abstract get _name(): string; } /** * @beta */ -export class AddFields implements Stage { - name = 'add_fields'; +export class AddFields extends Stage { + get _name(): string { return 'add_fields'; } + get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; - constructor(private fields: Map) {} + constructor(private fields: Map, options: StageOptions) { + super(options); + } - /** - * @internal - * @private - */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toMapValue(serializer, this.fields)] + ...super._toProto(serializer), + args: [toMapValue(serializer, this.fields)], }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.fields, context); + } } /** * @beta */ -export class RemoveFields implements Stage { - name = 'remove_fields'; +export class RemoveFields extends Stage { + get _name(): string { + return 'remove_fields'; + } - constructor(private fields: Field[]) {} + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + }; + + constructor(private fields: Field[], options: StageOptions) { + super(options); + } /** * @internal @@ -83,22 +129,34 @@ export class RemoveFields implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: this.fields.map(f => f._toProto(serializer)) + ...super._toProto(serializer), + args: this.fields.map(f => f._toProto(serializer)), }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.fields, context); + } } /** * @beta */ -export class Aggregate implements Stage { - name = 'aggregate'; +export class Aggregate extends Stage { + get _name(): string { + return 'aggregate'; + } - constructor( - private accumulators: Map, - private groups: Map - ) {} + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + }; + + constructor(private groups: Map, + private accumulators: Map, + options: StageOptions) { + super(options); + } /** * @internal @@ -106,22 +164,36 @@ export class Aggregate implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: [ toMapValue(serializer, this.accumulators), toMapValue(serializer, this.groups) - ] + ], }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.groups, context); + readUserDataHelper(this.accumulators, context); + } } /** * @beta */ -export class Distinct implements Stage { - name = 'distinct'; +export class Distinct extends Stage { + get _name(): string { + return 'distinct'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + }; - constructor(private groups: Map) {} + constructor(private groups: Map, options: StageOptions) { + super(options); + } /** * @internal @@ -129,22 +201,40 @@ export class Distinct implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, + ...super._toProto(serializer), args: [toMapValue(serializer, this.groups)] }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.groups, context); + } } /** * @beta */ -export class CollectionSource implements Stage { - name = 'collection'; +export class CollectionSource extends Stage { + get _name(): string { + return 'collection'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + forceIndex: { + serverName: 'force_index', + }, + }); + }; + + private formattedCollectionPath: string; - constructor(private collectionPath: string) { - if (!this.collectionPath.startsWith('/')) { - this.collectionPath = '/' + this.collectionPath; - } + constructor(collection: string, options: StageOptions) { + super(options); + + // prepend slash to collection string + this.formattedCollectionPath = collection.startsWith('/') ? collection : '/' + collection; } /** @@ -153,19 +243,35 @@ export class CollectionSource implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [{ referenceValue: this.collectionPath }] + ...super._toProto(serializer), + args: [{referenceValue: this.formattedCollectionPath}], }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class CollectionGroupSource implements Stage { - name = 'collection_group'; +export class CollectionGroupSource extends Stage { + get _name(): string { + return 'collection_group'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + forceIndex: { + serverName: 'force_index', + }, + }); + }; - constructor(private collectionId: string) {} + constructor(private collectionId: string, options: StageOptions) { + super(options); + } /** * @internal @@ -173,17 +279,23 @@ export class CollectionGroupSource implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [{ referenceValue: '' }, { stringValue: this.collectionId }] + ...super._toProto(serializer), + args: [{referenceValue: ''}, {stringValue: this.collectionId}], }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } + /** * @beta */ -export class DatabaseSource implements Stage { - name = 'database'; +export class DatabaseSource extends Stage { + get _name(): string { return 'database'; } + get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; /** * @internal @@ -191,29 +303,32 @@ export class DatabaseSource implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name + ...super._toProto(serializer) }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class DocumentsSource implements Stage { - name = 'documents'; - - constructor(private docPaths: string[]) {} - - static of(refs: Array): DocumentsSource { - return new DocumentsSource( - refs.map(ref => - ref instanceof DocumentReference - ? '/' + ref.path - : ref.startsWith('/') - ? ref - : '/' + ref - ) - ); +export class DocumentsSource extends Stage { + get _name(): string { + return 'documents'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + }; + + private formattedPaths: string[]; + + constructor(docPaths: string[], options: StageOptions) { + super(options); + this.formattedPaths = docPaths.map(path => path.startsWith('/') ? path : '/' + path); } /** @@ -222,21 +337,27 @@ export class DocumentsSource implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: this.docPaths.map(p => { - return { referenceValue: p }; + ...super._toProto(serializer), + args: this.formattedPaths.map(p => { + return {referenceValue: p}; }) }; } + + _readUserData(context: ParseContext): void { + super._readUserData(context); + } } /** * @beta */ -export class Where implements Stage { - name = 'where'; +export class Where extends Stage { + get _name(): string { return 'where'; } + get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; - constructor(private condition: BooleanExpr) {} + constructor(private condition: BooleanExpr, options: StageOptions) { + super(options);} /** * @internal @@ -244,87 +365,78 @@ export class Where implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [(this.condition as unknown as Expr)._toProto(serializer)] + ...super._toProto(serializer), + args: [this.condition._toProto(serializer)], }; } -} -/** - * @beta - */ -export interface FindNearestOptions { - field: Field | string; - vectorValue: VectorValue | number[]; - distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; - limit?: number; - distanceField?: string; + _readUserData(context: ParseContext): void { + super._readUserData(context); + readUserDataHelper(this.condition, context); + } } /** * @beta */ -export class FindNearest implements Stage { - name = 'find_nearest'; +export class FindNearest extends Stage { + get _name(): string { + return 'find_nearest'; + } - /** - * @private - * @internal - * - * @param _field - * @param _vectorValue - * @param _distanceMeasure - * @param _limit - * @param _distanceField - */ - constructor( - private _field: Field, - private _vectorValue: ObjectValue, - private _distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', - private _limit?: number, - private _distanceField?: string - ) {} + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + limit: { + serverName: 'limit', + }, + distanceField: { + serverName: 'distance_field', + }, + }); + }; + + constructor(private vectorValue: Expr, + private field: Field, + private distanceMeasure: "euclidean" | "cosine" | "dot_product", options: StageOptions) { + super(options); + } /** * @private * @internal */ _toProto(serializer: JsonProtoSerializer): ProtoStage { - const options: { [k: string]: ProtoValue } = {}; - - if (this._limit) { - options.limit = toNumber(serializer, this._limit)!; - } - - if (this._distanceField) { - // eslint-disable-next-line camelcase - options.distance_field = field(this._distanceField)._toProto(serializer); - } - return { - name: this.name, + ...super._toProto(serializer), args: [ - this._field._toProto(serializer), - this._vectorValue.value, - toStringValue(this._distanceMeasure) + this.field._toProto(serializer), + this.vectorValue._toProto(serializer), + toStringValue(this.distanceMeasure) ], - options }; } + + _readUserData(context: ParseContext) : void { + super._readUserData(context); + readUserDataHelper(this.vectorValue, context); + readUserDataHelper(this.field, context); + } } /** * @beta */ -export class Limit implements Stage { - name = 'limit'; +export class Limit extends Stage { + get _name(): string { return 'limit'; } + get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; - constructor(readonly limit: number) { + constructor(private limit: number, options: StageOptions) { hardAssert( !isNaN(limit) && limit !== Infinity && limit !== -Infinity, 0x882c, 'Invalid limit value' ); + super(options); } /** @@ -333,8 +445,8 @@ export class Limit implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toNumber(serializer, this.limit)] + ...super._toProto(serializer), + args: [toNumber(serializer, this.limit)], }; } } @@ -342,10 +454,12 @@ export class Limit implements Stage { /** * @beta */ -export class Offset implements Stage { - name = 'offset'; +export class Offset extends Stage { + get _name(): string { return 'offset'; } + get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; - constructor(private offset: number) {} + constructor(private offset: number, options: StageOptions) { + super(options);} /** * @internal @@ -353,8 +467,8 @@ export class Offset implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toNumber(serializer, this.offset)] + ...super._toProto(serializer), + args: [toNumber(serializer, this.offset)], }; } } @@ -362,10 +476,12 @@ export class Offset implements Stage { /** * @beta */ -export class Select implements Stage { - name = 'select'; +export class Select extends Stage { + get _name(): string { return 'select'; } + get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; - constructor(private projections: Map) {} + constructor(private selections: Map, options: StageOptions) { + super(options);} /** * @internal @@ -373,19 +489,32 @@ export class Select implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toMapValue(serializer, this.projections)] + ...super._toProto(serializer), + args: [toMapValue(serializer, this.selections)], }; } + + _readUserData(context: ParseContext) : void { + super._readUserData(context); + readUserDataHelper(this.selections, context); + } } /** * @beta */ -export class Sort implements Stage { - name = 'sort'; +export class Sort extends Stage { + get _name(): string { + return 'sort'; + } - constructor(private orders: Ordering[]) {} + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + }; + + constructor(private orderings: Ordering[], options: StageOptions) { + super(options); + } /** * @internal @@ -393,105 +522,149 @@ export class Sort implements Stage { */ _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: this.orders.map(o => o._toProto(serializer)) + ...super._toProto(serializer), + args: this.orderings.map(o => o._toProto(serializer)) }; } + + _readUserData(context: ParseContext) : void { + super._readUserData(context); + readUserDataHelper(this.orderings, context); + } } /** * @beta */ -export class Sample implements Stage { - name = 'sample'; +export class Sample extends Stage { + get _name(): string { return 'sample'; } + get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; - constructor(private limit: number, private mode: string) {} + constructor(private rate: number, + private mode: 'percent' | 'documents', options: StageOptions) { + super(options);} _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toNumber(serializer, this.limit)!, toStringValue(this.mode)!] + ...super._toProto(serializer), + args: [toNumber(serializer, this.rate)!, toStringValue(this.mode)!] }; } + + _readUserData(context: ParseContext) : void { + super._readUserData(context); + } } /** * @beta */ -export class Union implements Stage { - name = 'union'; +export class Union extends Stage { + get _name(): string { + return 'union'; + } - constructor(private _other: Pipeline) {} + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + }; + + constructor(private other: Pipeline, options: StageOptions) { + super(options); + } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [toPipelineValue(this._other._toProto(serializer))] + ...super._toProto(serializer), + args: [toPipelineValue(this.other._toProto(serializer))], }; } + + _readUserData(context: ParseContext) : void { + super._readUserData(context); + } } /** * @beta */ -export class Unnest implements Stage { - name = 'unnest'; - constructor( - private expr: Expr, - private alias: Field, - private indexField?: Field - ) {} +export class Unnest extends Stage { + get _name(): string { + return 'unnest'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({ + indexField: { + serverName: 'index_field', + }, + }); + }; + + constructor(private alias: string, + private expr: Expr, + options: StageOptions) { + super(options); + } _toProto(serializer: JsonProtoSerializer): ProtoStage { - const stageProto: ProtoStage = { - name: this.name, - args: [this.expr._toProto(serializer), this.alias._toProto(serializer)] + return { + ...super._toProto(serializer), + args: [this.expr._toProto(serializer), field(this.alias)._toProto(serializer)], }; + } - if (this.indexField) { - stageProto.options = { - ['index_field']: this.indexField._toProto(serializer) - }; - } - - return stageProto; + _readUserData(context: ParseContext) : void { + super._readUserData(context); + readUserDataHelper(this.expr, context); } } /** * @beta */ -export class Replace implements Stage { - name = 'replace_with'; +export class Replace extends Stage { + static readonly MODE = 'full_replace'; - constructor( - private field: Expr, - private mode: - | 'full_replace' - | 'merge_prefer_nest' - | 'merge_prefer_parent' = 'full_replace' - ) {} + get _name(): string { + return 'replace_with'; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + }; + + constructor(private map: Expr, options: StageOptions) { + super(options); + } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { - name: this.name, - args: [this.field._toProto(serializer), toStringValue(this.mode)] + ...super._toProto(serializer), + args: [this.map._toProto(serializer), toStringValue(Replace.MODE)], }; } + + _readUserData(context: ParseContext) : void { + super._readUserData(context); + readUserDataHelper(this.map, context); + } } /** * @beta */ -export class GenericStage implements Stage { +export class GenericStage extends Stage { /** * @private * @internal */ constructor( - public name: string, - private params: Array - ) {} + private name: string, + private params: Array, + rawOptions: Record + ) { + super({rawOptions}); + } /** * @internal @@ -500,7 +673,47 @@ export class GenericStage implements Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { name: this.name, - args: this.params.map(o => o._toProto(serializer)) + args: this.params.map(o => o._toProto(serializer)), + options: this.optionsProto }; } + + _readUserData(context: ParseContext) : void { + super._readUserData(context); + readUserDataHelper(this.params, context); + } + + get _name(): string { + return this.name; + } + + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } +} + + +/** + * Helper to read user data across a number of different formats. + * @param name Name of the calling function. Used for error messages when invalid user data is encountered. + * @param expressionMap + * @return the expressionMap argument. + * @private + */ +function readUserDataHelper< +T extends +| Map +| UserData[] +| UserData +>(expressionMap: T, context: ParseContext): T { + if (isUserData(expressionMap)) { + expressionMap._readUserData(context); + } else if (Array.isArray(expressionMap)) { + expressionMap.forEach(readableData => + readableData._readUserData(context) + ); + } else { + expressionMap.forEach(expr => expr._readUserData(context)); + } + return expressionMap; } diff --git a/packages/firestore/src/lite-api/stage_options.ts b/packages/firestore/src/lite-api/stage_options.ts new file mode 100644 index 00000000000..66c47fba1e9 --- /dev/null +++ b/packages/firestore/src/lite-api/stage_options.ts @@ -0,0 +1,281 @@ +import {OneOf} from "../util/types"; + +import { + AggregateWithAlias, + BooleanExpr, + Expr, + Field, + Ordering, + Selectable +} from "./expressions"; +import {Pipeline} from "./pipeline"; +import {CollectionReference, DocumentReference} from "./reference"; +import {VectorValue} from "./vector_value"; + + +/** + * Options defining how a Stage is evaluated. + */ +export interface StageOptions { + /** + * An escape hatch to set options not known at SDK build time. These values + * will be passed directly to the Firestore backend and not used by the SDK. + * + * The option name will be used as provided. And must match the name + * format used by the backend (hint: use a snake_case_name). + * + * Raw option values can be any type supported + * by Firestore (for example: string, boolean, number, map, …). Value types + * not known to the SDK will be rejected. + * + * Values specified in rawOptions will take precedence over any options + * with the same name set by the SDK. + * + * `rawOptions` supports dot notation, if you want to override + * a nested option. + */ + rawOptions?: { + [name: string]: unknown; + }; +} +/** + * Options defining how a CollectionStage is evaluated. See {@link PipelineSource.collection}. + */ +export type CollectionStageOptions = StageOptions & { + /** + * Name or reference to the collection that will be used as the Pipeline source. + */ + collection: string | CollectionReference; + + /** + * Specifies the name of an index to be used for a query, overriding the query optimizer's default choice. + * This can be useful for performance tuning in specific scenarios where the default index selection + * does not yield optimal performance. + * + * @remarks This property is optional. When provided, it should be the exact name of the index to force. + */ + forceIndex?: string; +}; + +/** + * Defines the configuration options for a {@link CollectionGroupStage} within a pipeline. + * This type extends {@link StageOptions} and provides specific settings for how a collection group + * is identified and processed during pipeline execution. + * + * @see {@link PipelineSource.collectionGroup} to create a collection group stage. + */ +export type CollectionGroupStageOptions = StageOptions & { + /** + * ID of the collection group to use as the Pipeline source. + */ + collectionId: string; + + /** + * Specifies the name of an index to be used for a query, overriding the query optimizer's default choice. + * This can be useful for performance tuning in specific scenarios where the default index selection + * does not yield optimal performance. + * + * @remarks This property is optional. When provided, it should be the exact name of the index to force. + */ + forceIndex?: string; +}; +/** + * Options defining how a DatabaseStage is evaluated. See {@link PipelineSource.database}. + */ +export type DatabaseStageOptions = StageOptions & {}; +/** + * Options defining how a DocumentsStage is evaluated. See {@link PipelineSource.documents}. + */ +export type DocumentsStageOptions = StageOptions & { + /** + * An array of paths and DocumentReferences specifying the individual documents that will be the source of this pipeline. + * The converters for these DocumentReferences will be ignored and not have an effect on this pipeline. + * There must be at least one document specified in the array. + */ + docs: Array; +}; +/** + * Options defining how an AddFieldsStage is evaluated. See {@link Pipeline.addFields}. + */ +export type AddFieldsStageOptions = StageOptions & { + /** + * The fields to add to each document, specified as a {@link Selectable}. + * At least one field is required. + */ + fields: Selectable[]; +}; +/** + * Options defining how a RemoveFieldsStage is evaluated. See {@link Pipeline.removeFields}. + */ +export type RemoveFieldsStageOptions = StageOptions & { + /** + * The fields to remove from each document. + */ + fields: Array; +}; +/** + * Options defining how a SelectStage is evaluated. See {@link Pipeline.select}. + */ +export type SelectStageOptions = StageOptions & { + /** + * The fields to include in the output documents, specified as {@link Selectable} expression + * or as a string value indicating the field name. + */ + selections: Array; +}; +/** + * Options defining how a WhereStage is evaluated. See {@link Pipeline.where}. + */ +export type WhereStageOptions = StageOptions & { + /** + * The {@link BooleanExpr} to apply as a filter for each input document to this stage. + */ + condition: BooleanExpr; +}; +/** + * Options defining how an OffsetStage is evaluated. See {@link Pipeline.offset}. + */ +export type OffsetStageOptions = StageOptions & { + /** + * The number of documents to skip. + */ + offset: number; +}; +/** + * Options defining how a LimitStage is evaluated. See {@link Pipeline.limit}. + */ +export type LimitStageOptions = StageOptions & { + /** + * The maximum number of documents to return. + */ + limit: number; +}; +/** + * Options defining how a DistinctStage is evaluated. See {@link Pipeline.distinct}. + */ +export type DistinctStageOptions = StageOptions & { + /** + * The {@link Selectable} expressions or field names to consider when determining + * distinct value combinations (groups). + */ + groups: Array; +}; + +/** + * Options defining how an AggregateStage is evaluated. See {@link Pipeline.aggregate}. + */ +export type AggregateStageOptions = StageOptions & { + /** + * The {@link AliasedAggregate} values specifying aggregate operations to + * perform on the input documents. + */ + accumulators: AggregateWithAlias[]; + /** + * The {@link Selectable} expressions or field names to consider when determining + * distinct value combinations (groups), which will be aggregated over. + */ + groups?: Array; +}; +/** + * Options defining how a FindNearestStage is evaluated. See {@link Pipeline.findNearest}. + */ +export type FindNearestStageOptions = StageOptions & { + /** + * Specifies the field to be used. This can be a string representing the field path + * (e.g., 'fieldName', 'nested.fieldName') or an object of type {@link Field} + * representing a more complex field expression. + */ + field: Field | string; + /** + * Specifies the query vector value, to which the vector distance will be computed. + */ + vectorValue: VectorValue | number[]; + /** + * Specifies the method used to compute the distance between vectors. + * + * Possible values are: + * - `'euclidean'`: Euclidean distance. + * - `'cosine'`: Cosine similarity. + * - `'dot_product'`: Dot product. + */ + distanceMeasure: 'euclidean' | 'cosine' | 'dot_product'; + /** + * The maximum number of documents to return from the FindNearest stage. + */ + limit?: number; + /** + * If set, specifies the field on the output documents that will contain + * the computed vector distance for the document. If not set, the computed + * vector distance will not be returned. + */ + distanceField?: string; +}; +/** + * Options defining how a ReplaceWithStage is evaluated. See {@link Pipeline.replaceWith}. + */ +export type ReplaceWithStageOptions = StageOptions & { + /** + * The name of a field that contains a map or an {@link Expr} that + * evaluates to a map. + */ + map: Expr | string; +}; +/** + * Defines the options for evaluating a sample stage within a pipeline. + * This type combines common {@link StageOptions} with a specific configuration + * where only one of the defined sampling methods can be applied. + * + * See {@link Pipeline.sample} to create a sample stage.. + */ +export type SampleStageOptions = StageOptions & + OneOf<{ + /** + * If set, specifies the sample rate as a percentage of the + * input documents. + * + * Cannot be set when `documents: number` is set. + */ + percentage: number; + /** + * If set, specifies the sample rate as a total number of + * documents to sample from the input documents. + * + * Cannot be set when `percentage: number` is set. + */ + documents: number; + }>; +/** + * Options defining how a UnionStage is evaluated. See {@link Pipeline.union}. + */ +export type UnionStageOptions = StageOptions & { + /** + * Specifies the other Pipeline to union with. + */ + other: Pipeline; +}; + +/** + * Represents the specific options available for configuring an `UnnestStage` within a pipeline. + */ +export type UnnestStageOptions = StageOptions & { + /** + * A `Selectable` object that defines an array expression to be un-nested + * and the alias for the un-nested field. + */ + selectable: Selectable; + /** + * If set, specifies the field on the output documents that will contain the + * offset (starting at zero) that the element is from the original array. + */ + indexField?: string; +}; +/** + * Options defining how a SortStage is evaluated. See {@link Pipeline.sort}. + */ +export type SortStageOptions = StageOptions & { + /** + * Orderings specify how the input documents are sorted. + * One or more ordering are required. + */ + orderings: Ordering[]; +}; diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index 905b13edd6f..8ea2728b3a7 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -1079,3 +1079,11 @@ function fieldMaskContains( ): boolean { return haystack.some(v => v.isEqual(needle)); } + +export interface UserData { + _readUserData(context: ParseContext): void; +} + +export function isUserData(value: unknown): value is UserData { + return typeof (value as UserData)._readUserData === 'function'; +} diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index d08e0d16874..1f20d0474e0 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { ParseContext } from '../api/parse_context'; import { Aggregate } from '../core/aggregate'; import { Bound } from '../core/bound'; import { DatabaseId } from '../core/database_info'; @@ -41,7 +40,6 @@ import { TargetId } from '../core/types'; import { Bytes } from '../lite-api/bytes'; import { GeoPoint } from '../lite-api/geo_point'; import { Timestamp } from '../lite-api/timestamp'; -import { UserDataReader } from '../lite-api/user_data_reader'; import { TargetData, TargetPurpose } from '../local/target_data'; import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -1475,9 +1473,6 @@ export function isProtoValueSerializable( ); } -export interface UserData { - _readUserData(dataReader: UserDataReader, context?: ParseContext): void; -} export function toMapValue( serializer: JsonProtoSerializer, diff --git a/packages/firestore/src/util/pipeline_util.ts b/packages/firestore/src/util/pipeline_util.ts new file mode 100644 index 00000000000..430eff8e51f --- /dev/null +++ b/packages/firestore/src/util/pipeline_util.ts @@ -0,0 +1,114 @@ +import { vector} from "../api"; +import { + _constant, + AggregateFunction, AggregateWithAlias, array, constant, + Expr, + ExprWithAlias, + field, + Field, map, + Selectable +} from "../lite-api/expressions"; +import {VectorValue} from "../lite-api/vector_value"; + +import {isPlainObject} from "./input_validation"; +import {isFirestoreValue} from "./proto"; +import {isString} from "./types"; + +export function selectablesToMap( + selectables: Array +): Map { + const result = new Map(); + for (const selectable of selectables) { + if (typeof selectable === 'string') { + result.set(selectable as string, field(selectable)); + } else if (selectable instanceof Field) { + result.set(selectable.alias, selectable.expr); + } else if (selectable instanceof ExprWithAlias) { + result.set(selectable.alias, selectable.expr); + } + } + return result; +} + +export function aliasedAggregateToMap( + aliasedAggregatees: AggregateWithAlias[] +): Map { + return aliasedAggregatees.reduce( + ( + map: Map, + selectable: AggregateWithAlias + ) => { + map.set(selectable.alias, selectable.aggregate as AggregateFunction); + return map; + }, + new Map() as Map + ); +} + +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * + * @private + * @internal + * @param value + */ +export function vectorToExpr( + value: VectorValue | number[] | Expr +): Expr { + if (value instanceof Expr) { + return value; + } else if (value instanceof VectorValue) { + const result = constant(value); + return result; + } else if (Array.isArray(value)) { + const result = constant(vector(value)); + return result; + } else { + throw new Error('Unsupported value: ' + typeof value); + } +} + +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * If the input is a string, it is assumed to be a field name, and a + * field(value) is returned. + * + * @private + * @internal + * @param value + */ +export function fieldOrExpression(value: unknown): Expr { + if (isString(value)) { + const result = field(value); + return result; + } else { + return valueToDefaultExpr(value); + } +} +/** + * Converts a value to an Expr, Returning either a Constant, MapFunction, + * ArrayFunction, or the input itself (if it's already an expression). + * + * @private + * @internal + * @param value + */ +export function valueToDefaultExpr(value: unknown): Expr { + let result: Expr | undefined; + if (isFirestoreValue(value)) { + return constant(value); + } + if (value instanceof Expr) { + return value; + } else if (isPlainObject(value)) { + result = map(value as Record); + } else if (value instanceof Array) { + result = array(value); + } else { + result = _constant(value, undefined); + } + + return result; +} diff --git a/packages/firestore/src/util/types.ts b/packages/firestore/src/util/types.ts index 361ebda1935..5cd3fab71ca 100644 --- a/packages/firestore/src/util/types.ts +++ b/packages/firestore/src/util/types.ts @@ -15,6 +15,15 @@ * limitations under the License. */ +import {CollectionReference} from "../api"; +import { + AggregateFunction, + AggregateWithAlias, BooleanExpr, Expr, field, Field, + Ordering, + Selectable +} from "../lite-api/expressions"; +import {Pipeline as LitePipeline} from "../lite-api/pipeline"; + /** Sentinel value that sorts before any Mutation Batch ID. */ export const BATCHID_UNKNOWN = -1; @@ -69,3 +78,80 @@ export interface DocumentLike { addEventListener(type: string, listener: EventListener): void; removeEventListener(type: string, listener: EventListener): void; } + +/** + * Utility type to create an type that only allows one + * property of the Type param T to be set. + * + * type XorY = OneOf<{ x: unknown, y: unknown}> + * let a = { x: "foo" } // OK + * let b = { y: "foo" } // OK + * let c = { a: "foo", y: "foo" } // Not OK + */ +export type OneOf = { + [K in keyof T]: Pick & { + [P in Exclude]?: undefined; +}; +}[keyof T]; + +export function isSelectable( + val: unknown +): val is Selectable { + const candidate = val as Selectable; + return ( + candidate.selectable && isString(candidate.alias) && isExpr(candidate.expr) + ); +} + + +export function isOrdering(val: unknown): val is Ordering { + const candidate = val as Ordering; + return ( + isExpr(candidate.expr) && + (candidate.direction === 'ascending' || + candidate.direction === 'descending') + ); +} + +export function isAliasedAggregate( + val: unknown +): val is AggregateWithAlias { + const candidate = val as AggregateWithAlias; + return ( + isString(candidate.alias) && + candidate.aggregate instanceof AggregateFunction + ); +} + +export function isExpr(val: unknown): val is Expr { + return val instanceof Expr; +} + +export function isBooleanExpr( + val: unknown +): val is BooleanExpr { + return val instanceof BooleanExpr; +} + +export function isField(val: unknown): val is Field { + return val instanceof Field; +} + +export function isLitePipeline(val: unknown): val is LitePipeline { + return val instanceof LitePipeline; +} + +export function isCollectionReference( + val: unknown +): val is CollectionReference { + return val instanceof CollectionReference; +} + +export function toField(value: string | Field): Field { + if (isString(value)) { + const result = field(value); + return result; + } else { + return value as Field; + } +} diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 4bfe2539f98..e91c828be44 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -20,7 +20,7 @@ import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { - AggregateFunction, + AggregateFunction, arrayGet, ascending, BooleanExpr, byteLength, @@ -54,7 +54,7 @@ import { collection, documentId as documentIdFieldPath, writeBatch, - addDoc + addDoc, } from '../util/firebase_export'; import { apiDescribe, withTestCollection, itIf } from '../util/helpers'; import { @@ -116,7 +116,6 @@ import { charLength, bitRightShift, rand, - arrayOffset, minimum, maximum, isError, @@ -135,8 +134,7 @@ import { xor, field, constant, - _internalPipelineToExecutePipelineRequestProto, - FindNearestOptions + _internalPipelineToExecutePipelineRequestProto, FindNearestStageOptions, } from '../util/pipeline_export'; use(chaiAsPromised); @@ -146,7 +144,7 @@ setLogLevel('debug'); const testUnsupportedFeatures: boolean | 'only' = false; const timestampDeltaMS = 1000; -apiDescribe('Pipelines', persistence => { +apiDescribe.only('Pipelines', persistence => { addEqualityMatcher(); let firestore: Firestore; @@ -835,6 +833,33 @@ apiDescribe('Pipelines', persistence => { }); }); + it('supports aggregate options', async () => { + let snapshot = await execute(firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [countAll().as('count')], + })); + expectResults(snapshot, {count: 10}); + + snapshot = await execute(firestore + .pipeline() + .collection(randomCol.path) + .where(eq('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + avg('rating').as('avgRating'), + maximum('rating').as('maxRating'), + sum('rating').as('sumRating') + )); + expectResults(snapshot, { + count: 2, + avgRating: 4.4, + maxRating: 4.6, + sumRating: 8.8, + }); + }); + it('rejects groups without accumulators', async () => { await expect( execute( @@ -936,6 +961,29 @@ apiDescribe('Pipelines', persistence => { { genre: 'Southern Gothic', author: 'Harper Lee' } ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .distinct({groups: ['genre', 'author']}) + .sort(field('genre').ascending(), field('author').ascending()) + ); + expectResults( + snapshot, + { genre: 'Dystopian', author: 'George Orwell' }, + { genre: 'Dystopian', author: 'Margaret Atwood' }, + { genre: 'Fantasy', author: 'J.R.R. Tolkien' }, + { genre: 'Magical Realism', author: 'Gabriel García Márquez' }, + { genre: 'Modernist', author: 'F. Scott Fitzgerald' }, + { genre: 'Psychological Thriller', author: 'Fyodor Dostoevsky' }, + { genre: 'Romance', author: 'Jane Austen' }, + { genre: 'Science Fiction', author: 'Douglas Adams' }, + { genre: 'Science Fiction', author: 'Frank Herbert' }, + { genre: 'Southern Gothic', author: 'Harper Lee' } + ); + }); }); describe('select stage', () => { @@ -967,6 +1015,23 @@ apiDescribe('Pipelines', persistence => { { title: "The Handmaid's Tale", author: 'Margaret Atwood' } ); }); + + it('supports options', async () => { + const snapshot = await execute(firestore + .pipeline() + .collection(randomCol.path) + .select({selections: ['title', field('author').as('auth0r')]}) + .sort(field('auth0r').ascending()) + .limit(2)); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + auth0r: 'Douglas Adams', + }, + {title: 'The Great Gatsby', auth0r: 'F. Scott Fitzgerald'} + ); + }); }); describe('addField stage', () => { @@ -1021,6 +1086,60 @@ apiDescribe('Pipelines', persistence => { } ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .addFields({ + fields: [constant('bar').as('foo')] + }) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + foo: 'bar' + }, + { + title: 'The Great Gatsby', + author: 'F. Scott Fitzgerald', + foo: 'bar' + }, + { title: 'Dune', author: 'Frank Herbert', foo: 'bar' }, + { + title: 'Crime and Punishment', + author: 'Fyodor Dostoevsky', + foo: 'bar' + }, + { + title: 'One Hundred Years of Solitude', + author: 'Gabriel García Márquez', + foo: 'bar' + }, + { title: '1984', author: 'George Orwell', foo: 'bar' }, + { + title: 'To Kill a Mockingbird', + author: 'Harper Lee', + foo: 'bar' + }, + { + title: 'The Lord of the Rings', + author: 'J.R.R. Tolkien', + foo: 'bar' + }, + { title: 'Pride and Prejudice', author: 'Jane Austen', foo: 'bar' }, + { + title: "The Handmaid's Tale", + author: 'Margaret Atwood', + foo: 'bar' + } + ); + }); }); describe('removeFields stage', () => { @@ -1062,6 +1181,46 @@ apiDescribe('Pipelines', persistence => { } ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .sort(field('author').ascending()) + .removeFields({fields: [field('author')] + }) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, + { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, + { title: 'Pride and Prejudice' }, + { + title: "The Handmaid's Tale" + } + ); + }); }); describe('where stage', () => { @@ -1141,17 +1300,18 @@ apiDescribe('Pipelines', persistence => { ); }); - // it('where with boolean constant', async () => { - // const snapshot = await execute( - // firestore - // .pipeline() - // .collection(randomCol.path) - // .where(constant(true)) - // .sort(ascending('__name__')) - // .limit(2) - // ); - // expectResults(snapshot, 'book1', 'book10'); - // }); + it('supports options', async () => { + const snapshot = await execute(firestore + .pipeline() + .collection(randomCol.path) + .where({ + condition: and( + gt('rating', 4.5), + eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ), + })); + expectResults(snapshot, 'book10', 'book4'); + }); }); describe('sort, offset, and limit stages', () => { @@ -1172,6 +1332,24 @@ apiDescribe('Pipelines', persistence => { { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } ); }); + + it('sort, offset, and limit stages support options', async () => { + const snapshot = await execute(firestore + .pipeline() + .collection(randomCol.path) + .sort({ + orderings: [field('author').ascending()], + }) + .offset({offset: 5}) + .limit({limit: 3}) + .select('title', 'author')); + expectResults( + snapshot, + {title: '1984', author: 'George Orwell'}, + {title: 'To Kill a Mockingbird', author: 'Harper Lee'}, + {title: 'The Lord of the Rings', author: 'J.R.R. Tolkien'} + ); + }); }); describe('generic stage', () => { @@ -1180,7 +1358,7 @@ apiDescribe('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .genericStage('select', [ + .rawStage('select', [ { title: field('title'), metadata: { @@ -1207,7 +1385,7 @@ apiDescribe('Pipelines', persistence => { .sort(field('author').ascending()) .limit(1) .select('title', 'author') - .genericStage('add_fields', [ + .rawStage('add_fields', [ { display: strConcat('title', ' - ', field('author')) } @@ -1226,7 +1404,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .genericStage('where', [field('author').eq('Douglas Adams')]) + .rawStage('where', [field('author').eq('Douglas Adams')]) ); expectResults(snapshot, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1240,14 +1418,14 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .genericStage('sort', [ + .rawStage('sort', [ { direction: 'ascending', expression: field('author') } ]) - .genericStage('offset', [3]) - .genericStage('limit', [1]) + .rawStage('offset', [3]) + .rawStage('limit', [1]) ); expectResults(snapshot, { author: 'Fyodor Dostoevsky', @@ -1261,7 +1439,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author', 'rating') - .genericStage('aggregate', [ + .rawStage('aggregate', [ { averageRating: field('rating').avg() }, {} ]) @@ -1277,7 +1455,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author', 'rating') - .genericStage('distinct', [{ rating: field('rating') }]) + .rawStage('distinct', [{ rating: field('rating') }]) .sort(field('rating').descending()) ); expectResults( @@ -1305,6 +1483,37 @@ apiDescribe('Pipelines', persistence => { } ); }); + + it('can perform FindNearest query', async () => { + const snapshot = await execute(firestore + .pipeline() + .collection(randomCol) + .rawStage( + 'find_nearest', + [ + field('embedding'), + vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), + 'euclidean', + ], + { + 'distance_field': field('computedDistance'), + limit: 2, + } + ) + .select('title', 'computedDistance')); + + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + computedDistance: 1, + }, + { + title: 'One Hundred Years of Solitude', + computedDistance: 12.041594578792296, + } + ); + }); }); describe('replaceWith stage', () => { @@ -1343,6 +1552,19 @@ apiDescribe('Pipelines', persistence => { baz: { title: "The Hitchhiker's Guide to the Galaxy" } }); }); + + it('supports options', async () => { + const snapshot = await execute(firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith({map: 'awards'})); + expectResults(snapshot, { + hugo: true, + nebula: false, + others: {unknown: {year: 1980}}, + }); + }); }); describe('sample stage', () => { @@ -1414,6 +1636,37 @@ apiDescribe('Pipelines', persistence => { 'book9' ); }); + + it('supports options', async () => { + const snapshot = await execute(firestore + .pipeline() + .collection(randomCol.path) + .union({other: firestore.pipeline().collection(randomCol.path)}) + .sort(field(documentIdFieldPath()).ascending())); + expectResults( + snapshot, + 'book1', + 'book1', + 'book10', + 'book10', + 'book2', + 'book2', + 'book3', + 'book3', + 'book4', + 'book4', + 'book5', + 'book5', + 'book6', + 'book6', + 'book7', + 'book7', + 'book8', + 'book8', + 'book9', + 'book9' + ); + }); }); describe('unnest stage', () => { @@ -1423,7 +1676,7 @@ apiDescribe('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) - .unnest(field('tags').as('tag'), 'tagsIndex') + .unnest(field('tags').as('tag')) .select( 'title', 'author', @@ -1485,6 +1738,81 @@ apiDescribe('Pipelines', persistence => { } ); }); + + it('unnest with index field', async () => { + const snapshot = await execute(firestore + .pipeline() + .collection(randomCol.path) + .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest({ + selectable: field('tags').as('tag'), + indexField: 'tagsIndex' + }) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField', + 'tagsIndex' + )); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'comedy', + awards: { + hugo: true, + nebula: false, + others: {unknown: {year: 1980}}, + }, + nestedField: {'level.1': {'level.2': true}}, + tagsIndex: 0, + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'space', + awards: { + hugo: true, + nebula: false, + others: {unknown: {year: 1980}}, + }, + nestedField: {'level.1': {'level.2': true}}, + tagsIndex: 1, + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'adventure', + awards: { + hugo: true, + nebula: false, + others: {unknown: {year: 1980}}, + }, + nestedField: {'level.1': {'level.2': true}}, + tagsIndex: 2, + } + ); + }); + it('unnest an expr', async () => { const snapshot = await execute( firestore @@ -1557,7 +1885,7 @@ apiDescribe('Pipelines', persistence => { describe('findNearest stage', () => { it('run pipeline with findNearest', async () => { - const measures: Array = [ + const measures: Array = [ 'euclidean', 'dot_product', 'cosine' @@ -1625,7 +1953,7 @@ apiDescribe('Pipelines', persistence => { const myPipeline = firestore .pipeline() .collection(randomCol.path) - .genericStage('select', [ + .rawStage('select', [ // incorrect parameter type field('title'), ]); @@ -1940,8 +2268,8 @@ apiDescribe('Pipelines', persistence => { subtract(field('published'), 1900).as('yearsSince1900'), field('rating').multiply(10).as('ratingTimesTen'), divide('rating', 2).as('ratingDividedByTwo'), - multiply('rating', 10, 2).as('ratingTimes20'), - add('rating', 1, 2).as('ratingPlus3'), + multiply('rating', 10).as('ratingTimes20'), + add('rating', 1).as('ratingPlus3'), mod('rating', 2).as('ratingMod2') ) .limit(1) @@ -2015,8 +2343,8 @@ apiDescribe('Pipelines', persistence => { .select( isNull('rating').as('ratingIsNull'), isNan('rating').as('ratingIsNaN'), - isError(arrayOffset('title', 0)).as('isError'), - ifError(arrayOffset('title', 0), constant('was error')).as( + isError(arrayGet('title', 0)).as('isError'), + ifError(arrayGet('title', 0), constant('was error')).as( 'ifError' ), isAbsent('foo').as('isAbsent'), @@ -2047,8 +2375,8 @@ apiDescribe('Pipelines', persistence => { .select( field('rating').isNull().as('ratingIsNull'), field('rating').isNan().as('ratingIsNaN'), - arrayOffset('title', 0).isError().as('isError'), - arrayOffset('title', 0) + arrayGet('title', 0).isError().as('isError'), + arrayGet('title', 0) .ifError(constant('was error')) .as('ifError'), field('foo').isAbsent().as('isAbsent'), @@ -2199,7 +2527,7 @@ apiDescribe('Pipelines', persistence => { ); }); - describe('genericFunction', () => { + describe('rawFunction', () => { it('add selectable', async () => { const snapshot = await execute( firestore @@ -2376,7 +2704,7 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(3) - .select(arrayOffset('tags', 0).as('firstTag')) + .select(arrayGet('tags', 0).as('firstTag')) ); const expectedResults = [ { @@ -2397,7 +2725,7 @@ apiDescribe('Pipelines', persistence => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(3) - .select(field('tags').arrayOffset(0).as('firstTag')) + .select(field('tags').arrayGet(0).as('firstTag')) ); expectResults(snapshot, ...expectedResults); }); diff --git a/packages/firestore/test/lite/pipeline.test.ts b/packages/firestore/test/lite/pipeline.test.ts index f919e1aa9de..08f45b609df 100644 --- a/packages/firestore/test/lite/pipeline.test.ts +++ b/packages/firestore/test/lite/pipeline.test.ts @@ -29,7 +29,6 @@ import { field, and, array, - arrayOffset, constant, add, subtract, @@ -114,7 +113,7 @@ import { not, toLower, toUpper, - trim + trim, arrayGet } from '../../src/lite-api/expressions'; import { documentId as documentIdFieldPath } from '../../src/lite-api/field_path'; import { vector } from '../../src/lite-api/field_value_impl'; @@ -131,7 +130,7 @@ import { doc } from '../../src/lite-api/reference'; import { addDoc, setDoc } from '../../src/lite-api/reference_impl'; -import { FindNearestOptions } from '../../src/lite-api/stage'; +import { FindNearestStageOptions } from '../../src/lite-api/stage_options'; import { Timestamp } from '../../src/lite-api/timestamp'; import { writeBatch } from '../../src/lite-api/write_batch'; import { itIf } from '../integration/util/helpers'; @@ -388,7 +387,7 @@ describe('Firestore Pipelines', () => { ); }); - it.only('returns execution time for an empty query', async () => { + it('returns execution time for an empty query', async () => { const start = new Date().valueOf(); const pipeline = firestore.pipeline().collection(randomCol.path).limit(0); @@ -1164,7 +1163,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .genericStage('select', [ + .rawStage('select', [ { title: field('title'), metadata: { @@ -1191,7 +1190,7 @@ describe('Firestore Pipelines', () => { .sort(field('author').ascending()) .limit(1) .select('title', 'author') - .genericStage('add_fields', [ + .rawStage('add_fields', [ { display: strConcat('title', ' - ', field('author')) } @@ -1210,7 +1209,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .genericStage('where', [field('author').eq('Douglas Adams')]) + .rawStage('where', [field('author').eq('Douglas Adams')]) ); expectResults(snapshot, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1224,14 +1223,14 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .genericStage('sort', [ + .rawStage('sort', [ { direction: 'ascending', expression: field('author') } ]) - .genericStage('offset', [3]) - .genericStage('limit', [1]) + .rawStage('offset', [3]) + .rawStage('limit', [1]) ); expectResults(snapshot, { author: 'Fyodor Dostoevsky', @@ -1245,7 +1244,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author', 'rating') - .genericStage('aggregate', [ + .rawStage('aggregate', [ { averageRating: field('rating').avg() }, {} ]) @@ -1261,7 +1260,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author', 'rating') - .genericStage('distinct', [{ rating: field('rating') }]) + .rawStage('distinct', [{ rating: field('rating') }]) .sort(field('rating').descending()) ); expectResults( @@ -1542,7 +1541,7 @@ describe('Firestore Pipelines', () => { describe('findNearest stage', () => { it('run pipeline with findNearest', async () => { - const measures: Array = [ + const measures: Array = [ 'euclidean', 'dot_product', 'cosine' @@ -1898,8 +1897,8 @@ describe('Firestore Pipelines', () => { subtract(field('published'), 1900).as('yearsSince1900'), field('rating').multiply(10).as('ratingTimesTen'), divide('rating', 2).as('ratingDividedByTwo'), - multiply('rating', 10, 2).as('ratingTimes20'), - add('rating', 1, 2).as('ratingPlus3'), + multiply('rating', 10).as('ratingTimes20'), + add('rating', 1).as('ratingPlus3'), mod('rating', 2).as('ratingMod2') ) .limit(1) @@ -1963,7 +1962,7 @@ describe('Firestore Pipelines', () => { ); }); - it.only('testChecks', async () => { + it('testChecks', async () => { let snapshot = await execute( firestore .pipeline() @@ -1973,8 +1972,8 @@ describe('Firestore Pipelines', () => { .select( isNull('rating').as('ratingIsNull'), isNan('rating').as('ratingIsNaN'), - isError(arrayOffset('title', 0)).as('isError'), - ifError(arrayOffset('title', 0), constant('was error')).as( + isError(arrayGet('title', 0)).as('isError'), + ifError(arrayGet('title', 0), constant('was error')).as( 'ifError' ), isAbsent('foo').as('isAbsent'), @@ -2005,8 +2004,8 @@ describe('Firestore Pipelines', () => { .select( field('rating').isNull().as('ratingIsNull'), field('rating').isNan().as('ratingIsNaN'), - arrayOffset('title', 0).isError().as('isError'), - arrayOffset('title', 0) + arrayGet('title', 0).isError().as('isError'), + arrayGet('title', 0) .ifError(constant('was error')) .as('ifError'), field('foo').isAbsent().as('isAbsent'), @@ -2334,7 +2333,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(3) - .select(arrayOffset('tags', 0).as('firstTag')) + .select(arrayGet('tags', 0).as('firstTag')) ); const expectedResults = [ { @@ -2355,7 +2354,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(3) - .select(field('tags').arrayOffset(0).as('firstTag')) + .select(field('tags').arrayGet(0).as('firstTag')) ); expectResults(snapshot, ...expectedResults); }); diff --git a/packages/firestore/test/unit/api/pipeline_impl.test.ts b/packages/firestore/test/unit/api/pipeline_impl.test.ts index 39d592f45b0..32476e6bee9 100644 --- a/packages/firestore/test/unit/api/pipeline_impl.test.ts +++ b/packages/firestore/test/unit/api/pipeline_impl.test.ts @@ -112,7 +112,8 @@ describe('execute(Pipeline|PipelineOptions)', () => { 'referenceValue': '/foo' } ], - 'name': 'collection' + 'name': 'collection', + 'options': {} } ] } @@ -148,7 +149,8 @@ describe('execute(Pipeline|PipelineOptions)', () => { 'referenceValue': '/foo' } ], - 'name': 'collection' + 'name': 'collection', + 'options': {} } ] } @@ -186,7 +188,8 @@ describe('execute(Pipeline|PipelineOptions)', () => { 'referenceValue': '/foo' } ], - 'name': 'collection' + 'name': 'collection', + 'options': {} } ] } diff --git a/packages/firestore/test/unit/core/options_util.test.ts b/packages/firestore/test/unit/core/options_util.test.ts index 3e7ed530313..523f3e4c11f 100644 --- a/packages/firestore/test/unit/core/options_util.test.ts +++ b/packages/firestore/test/unit/core/options_util.test.ts @@ -24,7 +24,7 @@ import { } from "../../../src/lite-api/user_data_reader"; import {testUserDataReader} from "../../util/helpers"; -describe.only('OptionsUtil', () => { +describe('OptionsUtil', () => { let context: ParseContext | undefined; beforeEach(async () => { context = testUserDataReader(false).createContext(UserDataSource.Argument, 'beforeEach'); diff --git a/packages/firestore/test/unit/core/structured_pipeline.test.ts b/packages/firestore/test/unit/core/structured_pipeline.test.ts index 7603b895c80..712b24b1ae9 100644 --- a/packages/firestore/test/unit/core/structured_pipeline.test.ts +++ b/packages/firestore/test/unit/core/structured_pipeline.test.ts @@ -15,25 +15,23 @@ * limitations under the License. */ -import { expect } from 'chai'; +import {expect} from 'chai'; import * as sinon from 'sinon'; -import { DatabaseId } from '../../../src/core/database_info'; +import {DatabaseId} from '../../../src/core/database_info'; import {StructuredPipeline, StructuredPipelineOptions} from '../../../src/core/structured_pipeline'; -import { Pipeline as PipelineProto } from '../../../src/protos/firestore_proto_api'; -import { - JsonProtoSerializer, - ProtoSerializable -} from '../../../src/remote/serializer'; +import {UserDataSource} from "../../../src/lite-api/user_data_reader"; +import {Pipeline as PipelineProto} from '../../../src/protos/firestore_proto_api'; +import {JsonProtoSerializer, ProtoSerializable} from '../../../src/remote/serializer'; import {testUserDataReader} from "../../util/helpers"; -describe.only('StructuredPipeline', () => { +describe('StructuredPipeline', () => { it('should serialize the pipeline argument', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; const structuredPipelineOptions = new StructuredPipelineOptions(); - structuredPipelineOptions._readUserData(testUserDataReader(false)); + structuredPipelineOptions._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); const structuredPipeline = new StructuredPipeline(pipeline, structuredPipelineOptions); const proto = structuredPipeline._toProto( @@ -56,7 +54,7 @@ describe.only('StructuredPipeline', () => { const options = new StructuredPipelineOptions({ indexMode: 'recommended' }); - options._readUserData(testUserDataReader(false)); + options._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); const structuredPipeline = new StructuredPipeline( pipeline,options ); @@ -87,7 +85,7 @@ describe.only('StructuredPipeline', () => { 'foo_bar': 'baz' } ); - options._readUserData(testUserDataReader(false)); + options._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); const structuredPipeline = new StructuredPipeline( pipeline, options @@ -119,7 +117,7 @@ describe.only('StructuredPipeline', () => { 'foo.bar': 'baz' } ); - options._readUserData(testUserDataReader(false)); + options._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); const structuredPipeline = new StructuredPipeline( pipeline, options @@ -157,7 +155,7 @@ describe.only('StructuredPipeline', () => { 'index_mode': 'baz' } ); - options._readUserData(testUserDataReader(false)); + options._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); const structuredPipeline = new StructuredPipeline( pipeline, options From 198f82991537f90147b5f654c554ef00f2c74c73 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:32:40 -0600 Subject: [PATCH 07/25] Rename and cleanup from api audit --- .../firestore/lite/pipelines/pipelines.ts | 65 ++++++------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/packages/firestore/lite/pipelines/pipelines.ts b/packages/firestore/lite/pipelines/pipelines.ts index b89a20cb3d3..6e4c0c994ff 100644 --- a/packages/firestore/lite/pipelines/pipelines.ts +++ b/packages/firestore/lite/pipelines/pipelines.ts @@ -84,25 +84,7 @@ export { } from '../../src/lite-api/stage_options'; export { - Stage, - AddFields, - Aggregate, - Distinct, - CollectionSource, - CollectionGroupSource, - DatabaseSource, - DocumentsSource, - Where, - FindNearest, - Limit, - Offset, - Select, - Sort, - GenericStage -} from '../../src/lite-api/stage'; - -export { - Expr, + Expression, field, and, array, @@ -110,23 +92,16 @@ export { add, subtract, multiply, - avg, - bitAnd, - substr, - constantVector, - bitLeftShift, - bitNot, + average, + substring, count, mapMerge, mapRemove, - bitOr, ifError, isAbsent, isError, or, rand, - bitRightShift, - bitXor, divide, isNotNan, map, @@ -134,42 +109,40 @@ export { isNull, mod, documentId, - eq, - neq, - lt, + equal, + notEqual, + lessThan, countIf, - lte, - gt, - gte, + lessThanOrEqual, + greaterThan, + greaterThanOrEqual, arrayConcat, arrayContains, arrayContainsAny, arrayContainsAll, arrayLength, - eqAny, - notEqAny, + equalAny, + notEqualAny, xor, - cond, + conditional, not, logicalMaximum, logicalMinimum, exists, isNan, reverse, - replaceFirst, - replaceAll, byteLength, charLength, like, regexContains, regexMatch, - strContains, + stringContains, startsWith, endsWith, toLower, toUpper, trim, - strConcat, + stringConcat, mapGet, countAll, minimum, @@ -185,17 +158,17 @@ export { unixSecondsToTimestamp, timestampToUnixSeconds, timestampAdd, - timestampSub, + timestampSubtract, ascending, descending, - ExprWithAlias, + AliasedExpression, Field, Constant, - FunctionExpr, + FunctionExpression, Ordering, - ExprType, + ExpressionType, AggregateWithAlias, Selectable, - BooleanExpr, + BooleanExpression, AggregateFunction } from '../../src/lite-api/expressions'; From 73b06151a9ec6d1123d2f8ba0af186132388a1d3 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:37:47 -0600 Subject: [PATCH 08/25] Rename and cleanup from api audit --- packages/firestore/src/api/pipeline_impl.ts | 46 +- packages/firestore/src/api_pipelines.ts | 51 +- packages/firestore/src/core/options_util.ts | 16 +- packages/firestore/src/core/pipeline-util.ts | 45 +- .../firestore/src/core/structured_pipeline.ts | 29 +- .../firestore/src/lite-api/expressions.ts | 3593 +++++++++-------- .../firestore/src/lite-api/pipeline-result.ts | 27 +- .../firestore/src/lite-api/pipeline-source.ts | 77 +- packages/firestore/src/lite-api/pipeline.ts | 366 +- .../firestore/src/lite-api/pipeline_impl.ts | 14 +- .../src/lite-api/pipeline_options.ts | 12 +- packages/firestore/src/lite-api/snapshot.ts | 2 +- packages/firestore/src/lite-api/stage.ts | 261 +- .../firestore/src/lite-api/stage_options.ts | 23 +- packages/firestore/src/remote/serializer.ts | 1 - .../firestore/src/util/input_validation.ts | 2 +- packages/firestore/src/util/pipeline_util.ts | 49 +- packages/firestore/src/util/types.ts | 35 +- .../test/integration/api/pipeline.test.ts | 1658 +++++--- packages/firestore/test/lite/pipeline.test.ts | 487 +-- .../test/unit/api/pipeline_impl.test.ts | 4 +- .../test/unit/core/options_util.test.ts | 136 +- .../unit/core/structured_pipeline.test.ts | 96 +- 23 files changed, 3771 insertions(+), 3259 deletions(-) diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts index ce415002a63..843a3696f71 100644 --- a/packages/firestore/src/api/pipeline_impl.ts +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -24,7 +24,7 @@ import { import { Pipeline as LitePipeline } from '../lite-api/pipeline'; import { PipelineResult, PipelineSnapshot } from '../lite-api/pipeline-result'; import { PipelineSource } from '../lite-api/pipeline-source'; -import { PipelineOptions } from '../lite-api/pipeline_options'; +import { PipelineExecuteOptions } from '../lite-api/pipeline_options'; import { Stage } from '../lite-api/stage'; import { newUserDataReader, @@ -78,19 +78,21 @@ declare module './database' { * @return A Promise representing the asynchronous pipeline execution. */ export function execute(pipeline: LitePipeline): Promise; -export function execute(options: PipelineOptions): Promise; export function execute( - pipelineOrOptions: LitePipeline | PipelineOptions + options: PipelineExecuteOptions +): Promise; +export function execute( + pipelineOrOptions: LitePipeline | PipelineExecuteOptions ): Promise { - const options: PipelineOptions = !( + const options: PipelineExecuteOptions = !( pipelineOrOptions instanceof LitePipeline ) ? pipelineOrOptions : { - pipeline: pipelineOrOptions - }; + pipeline: pipelineOrOptions + }; - const { pipeline, customOptions, ...rest } = options; + const { pipeline, rawOptions, ...rest } = options; const firestore = cast(pipeline._db, Firestore); const client = ensureFirestoreConfigured(firestore); @@ -101,7 +103,10 @@ export function execute( ); const context = udr.createContext(UserDataSource.Argument, 'execute'); - const structuredPipelineOptions = new StructuredPipelineOptions(rest, customOptions); + const structuredPipelineOptions = new StructuredPipelineOptions( + rest, + rawOptions + ); structuredPipelineOptions._readUserData(context); const structuredPipeline: StructuredPipeline = new StructuredPipeline( @@ -125,10 +130,10 @@ export function execute( element => new PipelineResult( pipeline._userDataWriter, + element.fields!, element.key?.path ? new DocumentReference(firestore, null, element.key) : undefined, - element.fields, element.createTime?.toTimestamp(), element.updateTime?.toTimestamp() ) @@ -141,14 +146,17 @@ export function execute( // Augment the Firestore class with the pipeline() factory method Firestore.prototype.pipeline = function (): PipelineSource { - const userDataReader = - newUserDataReader(this); - return new PipelineSource(this._databaseId, userDataReader, (stages: Stage[]) => { - return new Pipeline( - this, - userDataReader, - new ExpUserDataWriter(this), - stages - ); - }); + const userDataReader = newUserDataReader(this); + return new PipelineSource( + this._databaseId, + userDataReader, + (stages: Stage[]) => { + return new Pipeline( + this, + userDataReader, + new ExpUserDataWriter(this), + stages + ); + } + ); }; diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index d2b221c08b0..d7e1dc3dc64 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -29,7 +29,7 @@ export { Pipeline } from './api/pipeline'; export { execute } from './api/pipeline_impl'; -export { PipelineOptions } from './lite-api/pipeline_options'; +export { PipelineExecuteOptions } from './lite-api/pipeline_options'; export { StageOptions, @@ -68,7 +68,7 @@ export { Offset, Select, Sort, - GenericStage + RawStage } from './lite-api/stage'; export { @@ -79,46 +79,44 @@ export { multiply, divide, mod, - eq, - neq, - lt, - lte, - gt, - gte, + equal, + notEqual, + lessThan, + lessThanOrEqual, + greaterThan, + greaterThanOrEqual, arrayConcat, arrayContains, arrayContainsAny, arrayContainsAll, arrayLength, - eqAny, - notEqAny, + equalAny, + notEqualAny, xor, - cond, + conditional, not, logicalMaximum, logicalMinimum, exists, isNan, reverse, - replaceFirst, - replaceAll, byteLength, charLength, like, regexContains, regexMatch, - strContains, + stringContains, startsWith, endsWith, toLower, toUpper, trim, - strConcat, + stringConcat, mapGet, countAll, count, sum, - avg, + average, and, or, minimum, @@ -134,16 +132,10 @@ export { unixSecondsToTimestamp, timestampToUnixSeconds, timestampAdd, - timestampSub, + timestampSubtract, ascending, descending, countIf, - bitAnd, - bitOr, - bitXor, - bitNot, - bitLeftShift, - bitRightShift, rand, array, arrayGet, @@ -157,20 +149,19 @@ export { mapRemove, mapMerge, documentId, - substr, - Expr, - ExprWithAlias, + substring, + Expression, + AliasedExpression, Field, - Constant, - FunctionExpr, + FunctionExpression, Ordering } from './lite-api/expressions'; export type { - ExprType, + ExpressionType, AggregateWithAlias, Selectable, - BooleanExpr, + BooleanExpression, AggregateFunction } from './lite-api/expressions'; diff --git a/packages/firestore/src/core/options_util.ts b/packages/firestore/src/core/options_util.ts index b2b9adb6e45..eef30b6d84c 100644 --- a/packages/firestore/src/core/options_util.ts +++ b/packages/firestore/src/core/options_util.ts @@ -12,14 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ParseContext} from "../api/parse_context"; -import { - parseData, -} from "../lite-api/user_data_reader"; +import { ParseContext } from '../api/parse_context'; +import { parseData } from '../lite-api/user_data_reader'; import { ObjectValue } from '../model/object_value'; import { FieldPath } from '../model/path'; -import {ApiClientObjectMap, Value} from "../protos/firestore_proto_api"; -import {isPlainObject} from "../util/input_validation"; +import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; +import { isPlainObject } from '../util/input_validation'; import { mapToArray } from '../util/obj'; export type OptionsDefinitions = Record; export interface OptionDefinition { @@ -50,8 +48,8 @@ export class OptionsUtil { const nestedUtil = new OptionsUtil(optionDefinition.nestedOptions); protoValue = { mapValue: { - fields: nestedUtil.getOptionsProto(context, optionValue), - }, + fields: nestedUtil.getOptionsProto(context, optionValue) + } }; } else if (optionValue) { protoValue = parseData(optionValue, context) ?? undefined; @@ -82,7 +80,7 @@ export class OptionsUtil { const optionsMap = new Map( mapToArray(optionsOverride, (value, key) => [ FieldPath.fromServerFormat(key), - value !== undefined ? parseData(value, context) : null, + value !== undefined ? parseData(value, context) : null ]) ); result.setAll(optionsMap); diff --git a/packages/firestore/src/core/pipeline-util.ts b/packages/firestore/src/core/pipeline-util.ts index 7b810f091a2..3cf754e46f8 100644 --- a/packages/firestore/src/core/pipeline-util.ts +++ b/packages/firestore/src/core/pipeline-util.ts @@ -18,12 +18,12 @@ import { Firestore } from '../lite-api/database'; import { Constant, - BooleanExpr, + BooleanExpression, and, or, Ordering, - lt, - gt, + lessThan, + greaterThan, field } from '../lite-api/expressions'; import { Pipeline } from '../lite-api/pipeline'; @@ -50,7 +50,7 @@ import { /* eslint @typescript-eslint/no-explicit-any: 0 */ -export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { +export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpression { if (f instanceof FieldFilterInternal) { const fieldValue = field(f.field.toString()); if (isNanValue(f.value)) { @@ -72,32 +72,32 @@ export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { case Operator.LESS_THAN: return and( fieldValue.exists(), - fieldValue.lt(Constant._fromProto(value)) + fieldValue.lessThan(Constant._fromProto(value)) ); case Operator.LESS_THAN_OR_EQUAL: return and( fieldValue.exists(), - fieldValue.lte(Constant._fromProto(value)) + fieldValue.lessThanOrEqual(Constant._fromProto(value)) ); case Operator.GREATER_THAN: return and( fieldValue.exists(), - fieldValue.gt(Constant._fromProto(value)) + fieldValue.greaterThan(Constant._fromProto(value)) ); case Operator.GREATER_THAN_OR_EQUAL: return and( fieldValue.exists(), - fieldValue.gte(Constant._fromProto(value)) + fieldValue.greaterThanOrEqual(Constant._fromProto(value)) ); case Operator.EQUAL: return and( fieldValue.exists(), - fieldValue.eq(Constant._fromProto(value)) + fieldValue.equal(Constant._fromProto(value)) ); case Operator.NOT_EQUAL: return and( fieldValue.exists(), - fieldValue.neq(Constant._fromProto(value)) + fieldValue.notEqual(Constant._fromProto(value)) ); case Operator.ARRAY_CONTAINS: return and( @@ -109,11 +109,11 @@ export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { Constant._fromProto(val) ); if (!values) { - return and(fieldValue.exists(), fieldValue.eqAny([])); + return and(fieldValue.exists(), fieldValue.equalAny([])); } else if (values.length === 1) { - return and(fieldValue.exists(), fieldValue.eq(values[0])); + return and(fieldValue.exists(), fieldValue.equal(values[0])); } else { - return and(fieldValue.exists(), fieldValue.eqAny(values)); + return and(fieldValue.exists(), fieldValue.equalAny(values)); } } case Operator.ARRAY_CONTAINS_ANY: { @@ -127,11 +127,11 @@ export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpr { Constant._fromProto(val) ); if (!values) { - return and(fieldValue.exists(), fieldValue.notEqAny([])); + return and(fieldValue.exists(), fieldValue.notEqualAny([])); } else if (values.length === 1) { - return and(fieldValue.exists(), fieldValue.neq(values[0])); + return and(fieldValue.exists(), fieldValue.notEqual(values[0])); } else { - return and(fieldValue.exists(), fieldValue.notEqAny(values)); + return and(fieldValue.exists(), fieldValue.notEqualAny(values)); } } default: @@ -250,9 +250,9 @@ function whereConditionsFromCursor( bound: Bound, orderings: Ordering[], position: 'before' | 'after' -): BooleanExpr { +): BooleanExpression { // The filterFunc is either greater than or less than - const filterFunc = position === 'before' ? lt : gt; + const filterFunc = position === 'before' ? lessThan : greaterThan; const cursors = bound.position.map(value => Constant._fromProto(value)); const size = cursors.length; @@ -260,11 +260,11 @@ function whereConditionsFromCursor( let value = cursors[size - 1]; // Add condition for last bound - let condition: BooleanExpr = filterFunc(field, value); + let condition: BooleanExpression = filterFunc(field, value); if (bound.inclusive) { // When the cursor bound is inclusive, then the last bound // can be equal to the value, otherwise it's not equal - condition = or(condition, field.eq(value)); + condition = or(condition, field.equal(value)); } // Iterate backwards over the remaining bounds, adding @@ -276,7 +276,10 @@ function whereConditionsFromCursor( // For each field in the orderings, the condition is either // a) lt|gt the cursor value, // b) or equal the cursor value and lt|gt the cursor values for other fields - condition = or(filterFunc(field, value), and(field.eq(value), condition)); + condition = or( + filterFunc(field, value), + and(field.equal(value), condition) + ); } return condition; diff --git a/packages/firestore/src/core/structured_pipeline.ts b/packages/firestore/src/core/structured_pipeline.ts index b020db06e0a..ac8ee4284f6 100644 --- a/packages/firestore/src/core/structured_pipeline.ts +++ b/packages/firestore/src/core/structured_pipeline.ts @@ -15,35 +15,38 @@ * limitations under the License. */ -import {ParseContext} from "../api/parse_context"; -import {UserData} from "../lite-api/user_data_reader"; +import { ParseContext } from '../api/parse_context'; +import { UserData } from '../lite-api/user_data_reader'; import { - ApiClientObjectMap, firestoreV1ApiClientInterfaces, + ApiClientObjectMap, + firestoreV1ApiClientInterfaces, Pipeline as PipelineProto, StructuredPipeline as StructuredPipelineProto } from '../protos/firestore_proto_api'; -import { - JsonProtoSerializer, - ProtoSerializable, -} from '../remote/serializer'; +import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; -import {OptionsUtil} from "./options_util"; +import { OptionsUtil } from './options_util'; -export class StructuredPipelineOptions implements UserData{ +export class StructuredPipelineOptions implements UserData { proto: ApiClientObjectMap | undefined; readonly optionsUtil = new OptionsUtil({ indexMode: { - serverName: 'index_mode', + serverName: 'index_mode' } }); constructor( private _userOptions: Record = {}, - private _optionsOverride: Record = {}) {} + private _optionsOverride: Record = {} + ) {} _readUserData(context: ParseContext): void { - this.proto = this.optionsUtil.getOptionsProto(context, this._userOptions, this._optionsOverride); + this.proto = this.optionsUtil.getOptionsProto( + context, + this._userOptions, + this._optionsOverride + ); } } @@ -52,7 +55,7 @@ export class StructuredPipeline { constructor( private pipeline: ProtoSerializable, - private options: StructuredPipelineOptions, + private options: StructuredPipelineOptions ) {} _toProto(serializer: JsonProtoSerializer): StructuredPipelineProto { diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 8bd64a3680a..53f9ecf592a 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { vector } from '../api'; import { ParseContext } from '../api/parse_context'; import { DOCUMENT_KEY_NAME, @@ -25,7 +26,7 @@ import { JsonProtoSerializer, ProtoValueSerializable, toMapValue, - toStringValue, + toStringValue } from '../remote/serializer'; import { hardAssert } from '../util/assert'; import { isPlainObject } from '../util/input_validation'; @@ -37,10 +38,7 @@ import { documentId as documentIdFieldPath, FieldPath } from './field_path'; import { GeoPoint } from './geo_point'; import { DocumentReference } from './reference'; import { Timestamp } from './timestamp'; -import { - fieldPathFromArgument, - parseData, UserData, -} from './user_data_reader'; +import { fieldPathFromArgument, parseData, UserData } from './user_data_reader'; import { VectorValue } from './vector_value'; /** @@ -48,13 +46,13 @@ import { VectorValue } from './vector_value'; * * An enumeration of the different types of expressions. */ -export type ExprType = +export type ExpressionType = | 'Field' | 'Constant' | 'Function' | 'AggregateFunction' - | 'ListOfExprs' - | 'ExprWithAlias'; + | 'ListOfExpressions' + | 'AliasedExpression'; /** * Converts a value to an Expr, Returning either a Constant, MapFunction, @@ -64,9 +62,9 @@ export type ExprType = * @internal * @param value */ -function valueToDefaultExpr(value: unknown): Expr { - let result: Expr | undefined; - if (value instanceof Expr) { +function valueToDefaultExpr(value: unknown): Expression { + let result: Expression | undefined; + if (value instanceof Expression) { return value; } else if (isPlainObject(value)) { result = _map(value as Record, undefined); @@ -87,12 +85,15 @@ function valueToDefaultExpr(value: unknown): Expr { * @internal * @param value */ -function vectorToExpr(value: VectorValue | number[] | Expr): Expr { - if (value instanceof Expr) { +function vectorToExpr(value: VectorValue | number[] | Expression): Expression { + if (value instanceof Expression) { return value; + } else if (value instanceof VectorValue) { + return constant(value); + } else if (Array.isArray(value)) { + return constant(vector(value)); } else { - const result = constantVector(value); - return result; + throw new Error('Unsupported value: ' + typeof value); } } @@ -106,7 +107,7 @@ function vectorToExpr(value: VectorValue | number[] | Expr): Expr { * @internal * @param value */ -function fieldOrExpression(value: unknown): Expr { +function fieldOrExpression(value: unknown): Expression { if (isString(value)) { const result = field(value); return result; @@ -131,8 +132,8 @@ function fieldOrExpression(value: unknown): Expr { * The `Expr` class provides a fluent API for building expressions. You can chain together * method calls to create complex expressions. */ -export abstract class Expr implements ProtoValueSerializable, UserData { - abstract readonly exprType: ExprType; +export abstract class Expression implements ProtoValueSerializable, UserData { + abstract readonly expressionType: ExpressionType; abstract readonly _methodName?: string; @@ -147,9 +148,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @private * @internal */ - abstract _readUserData( - context: ParseContext - ): void; + abstract _readUserData(context: ParseContext): void; /** * Creates an expression that adds this expression to another expression. @@ -163,11 +162,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param others Optional additional expressions or literals to add to this expression. * @return A new `Expr` representing the addition operation. */ - add(second: Expr | unknown): FunctionExpr { - return new FunctionExpr('add', [ - this, - valueToDefaultExpr(second) - ], 'add'); + add(second: Expression | unknown): FunctionExpression { + return new FunctionExpression( + 'add', + [this, valueToDefaultExpr(second)], + 'add' + ); } /** @@ -178,10 +178,10 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("price").subtract(field("discount")); * ``` * - * @param other The expression to subtract from this expression. + * @param subtrahend The expression to subtract from this expression. * @return A new `Expr` representing the subtraction operation. */ - subtract(other: Expr): FunctionExpr; + subtract(subtrahend: Expression): FunctionExpression; /** * Creates an expression that subtracts a constant value from this expression. @@ -191,12 +191,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("total").subtract(20); * ``` * - * @param other The constant value to subtract. + * @param subtrahend The constant value to subtract. * @return A new `Expr` representing the subtraction operation. */ - subtract(other: number): FunctionExpr; - subtract(other: number | Expr): FunctionExpr { - return new FunctionExpr('subtract', [this, valueToDefaultExpr(other)], 'subtract'); + subtract(subtrahend: number): FunctionExpression; + subtract(subtrahend: number | Expression): FunctionExpression { + return new FunctionExpression( + 'subtract', + [this, valueToDefaultExpr(subtrahend)], + 'subtract' + ); } /** @@ -211,13 +215,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param others Optional additional expressions or literals to multiply by. * @return A new `Expr` representing the multiplication operation. */ - multiply( - second: Expr | number, - ): FunctionExpr { - return new FunctionExpr('multiply', [ - this, - valueToDefaultExpr(second) - ], 'multiply'); + multiply(second: Expression | number): FunctionExpression { + return new FunctionExpression( + 'multiply', + [this, valueToDefaultExpr(second)], + 'multiply' + ); } /** @@ -228,10 +231,10 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("total").divide(field("count")); * ``` * - * @param other The expression to divide by. + * @param divisor The expression to divide by. * @return A new `Expr` representing the division operation. */ - divide(other: Expr): FunctionExpr; + divide(divisor: Expression): FunctionExpression; /** * Creates an expression that divides this expression by a constant value. @@ -241,12 +244,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("value").divide(10); * ``` * - * @param other The constant value to divide by. + * @param divisor The constant value to divide by. * @return A new `Expr` representing the division operation. */ - divide(other: number): FunctionExpr; - divide(other: number | Expr): FunctionExpr { - return new FunctionExpr('divide', [this, valueToDefaultExpr(other)], 'divide'); + divide(divisor: number): FunctionExpression; + divide(divisor: number | Expression): FunctionExpression { + return new FunctionExpression( + 'divide', + [this, valueToDefaultExpr(divisor)], + 'divide' + ); } /** @@ -260,7 +267,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param expression The expression to divide by. * @return A new `Expr` representing the modulo operation. */ - mod(expression: Expr): FunctionExpr; + mod(expression: Expression): FunctionExpression; /** * Creates an expression that calculates the modulo (remainder) of dividing this expression by a constant value. @@ -273,9 +280,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param value The constant value to divide by. * @return A new `Expr` representing the modulo operation. */ - mod(value: number): FunctionExpr; - mod(other: number | Expr): FunctionExpr { - return new FunctionExpr('mod', [this, valueToDefaultExpr(other)], 'mod'); + mod(value: number): FunctionExpression; + mod(other: number | Expression): FunctionExpression { + return new FunctionExpression( + 'mod', + [this, valueToDefaultExpr(other)], + 'mod' + ); } /** @@ -283,28 +294,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'age' field is equal to 21 - * field("age").eq(21); + * field("age").equal(21); * ``` * * @param expression The expression to compare for equality. * @return A new `Expr` representing the equality comparison. */ - eq(expression: Expr): BooleanExpr; + equal(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is equal to a constant value. * * ```typescript * // Check if the 'city' field is equal to "London" - * field("city").eq("London"); + * field("city").equal("London"); * ``` * * @param value The constant value to compare for equality. * @return A new `Expr` representing the equality comparison. */ - eq(value: unknown): BooleanExpr; - eq(other: unknown): BooleanExpr { - return new BooleanExpr('eq', [this, valueToDefaultExpr(other)], 'eq'); + equal(value: unknown): BooleanExpression; + equal(other: unknown): BooleanExpression { + return new BooleanExpression( + 'equal', + [this, valueToDefaultExpr(other)], + 'equal' + ); } /** @@ -312,28 +327,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'status' field is not equal to "completed" - * field("status").neq("completed"); + * field("status").notEqual("completed"); * ``` * * @param expression The expression to compare for inequality. * @return A new `Expr` representing the inequality comparison. */ - neq(expression: Expr): BooleanExpr; + notEqual(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is not equal to a constant value. * * ```typescript * // Check if the 'country' field is not equal to "USA" - * field("country").neq("USA"); + * field("country").notEqual("USA"); * ``` * * @param value The constant value to compare for inequality. * @return A new `Expr` representing the inequality comparison. */ - neq(value: unknown): BooleanExpr; - neq(other: unknown): BooleanExpr { - return new BooleanExpr('neq', [this, valueToDefaultExpr(other)], 'neq'); + notEqual(value: unknown): BooleanExpression; + notEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'not_equal', + [this, valueToDefaultExpr(other)], + 'notEqual' + ); } /** @@ -341,28 +360,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'age' field is less than 'limit' - * field("age").lt(field('limit')); + * field("age").lessThan(field('limit')); * ``` * * @param experession The expression to compare for less than. * @return A new `Expr` representing the less than comparison. */ - lt(experession: Expr): BooleanExpr; + lessThan(experession: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is less than a constant value. * * ```typescript * // Check if the 'price' field is less than 50 - * field("price").lt(50); + * field("price").lessThan(50); * ``` * * @param value The constant value to compare for less than. * @return A new `Expr` representing the less than comparison. */ - lt(value: unknown): BooleanExpr; - lt(other: unknown): BooleanExpr { - return new BooleanExpr('lt', [this, valueToDefaultExpr(other)], 'lt'); + lessThan(value: unknown): BooleanExpression; + lessThan(other: unknown): BooleanExpression { + return new BooleanExpression( + 'less_than', + [this, valueToDefaultExpr(other)], + 'lessThan' + ); } /** @@ -371,28 +394,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * field("quantity").lte(constant(20)); + * field("quantity").lessThan(constant(20)); * ``` * * @param expression The expression to compare for less than or equal to. * @return A new `Expr` representing the less than or equal to comparison. */ - lte(expression: Expr): BooleanExpr; + lessThanOrEqual(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is less than or equal to a constant value. * * ```typescript * // Check if the 'score' field is less than or equal to 70 - * field("score").lte(70); + * field("score").lessThan(70); * ``` * * @param value The constant value to compare for less than or equal to. * @return A new `Expr` representing the less than or equal to comparison. */ - lte(value: unknown): BooleanExpr; - lte(other: unknown): BooleanExpr { - return new BooleanExpr('lte', [this, valueToDefaultExpr(other)], 'lte'); + lessThanOrEqual(value: unknown): BooleanExpression; + lessThanOrEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'less_than_or_equal', + [this, valueToDefaultExpr(other)], + 'lessThanOrEqual' + ); } /** @@ -400,28 +427,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'age' field is greater than the 'limit' field - * field("age").gt(field("limit")); + * field("age").greaterThan(field("limit")); * ``` * * @param expression The expression to compare for greater than. * @return A new `Expr` representing the greater than comparison. */ - gt(expression: Expr): BooleanExpr; + greaterThan(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is greater than a constant value. * * ```typescript * // Check if the 'price' field is greater than 100 - * field("price").gt(100); + * field("price").greaterThan(100); * ``` * * @param value The constant value to compare for greater than. * @return A new `Expr` representing the greater than comparison. */ - gt(value: unknown): BooleanExpr; - gt(other: unknown): BooleanExpr { - return new BooleanExpr('gt', [this, valueToDefaultExpr(other)], 'gt'); + greaterThan(value: unknown): BooleanExpression; + greaterThan(other: unknown): BooleanExpression { + return new BooleanExpression( + 'greater_than', + [this, valueToDefaultExpr(other)], + 'greaterThan' + ); } /** @@ -430,13 +461,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'quantity' field is greater than or equal to field 'requirement' plus 1 - * field("quantity").gte(field('requirement').add(1)); + * field("quantity").greaterThanOrEqual(field('requirement').add(1)); * ``` * * @param expression The expression to compare for greater than or equal to. * @return A new `Expr` representing the greater than or equal to comparison. */ - gte(expression: Expr): BooleanExpr; + greaterThanOrEqual(expression: Expression): BooleanExpression; /** * Creates an expression that checks if this expression is greater than or equal to a constant @@ -444,15 +475,19 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'score' field is greater than or equal to 80 - * field("score").gte(80); + * field("score").greaterThanOrEqual(80); * ``` * * @param value The constant value to compare for greater than or equal to. * @return A new `Expr` representing the greater than or equal to comparison. */ - gte(value: unknown): BooleanExpr; - gte(other: unknown): BooleanExpr { - return new BooleanExpr('gte', [this, valueToDefaultExpr(other)], 'gte'); + greaterThanOrEqual(value: unknown): BooleanExpression; + greaterThanOrEqual(other: unknown): BooleanExpression { + return new BooleanExpression( + 'greater_than_or_equal', + [this, valueToDefaultExpr(other)], + 'greaterThanOrEqual' + ); } /** @@ -467,12 +502,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @return A new `Expr` representing the concatenated array. */ arrayConcat( - secondArray: Expr | unknown[], - ...otherArrays: Array - ): FunctionExpr { + secondArray: Expression | unknown[], + ...otherArrays: Array + ): FunctionExpression { const elements = [secondArray, ...otherArrays]; const exprValues = elements.map(value => valueToDefaultExpr(value)); - return new FunctionExpr('array_concat', [this, ...exprValues], 'arrayConcat'); + return new FunctionExpression( + 'array_concat', + [this, ...exprValues], + 'arrayConcat' + ); } /** @@ -486,7 +525,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param expression The element to search for in the array. * @return A new `Expr` representing the 'array_contains' comparison. */ - arrayContains(expression: Expr): BooleanExpr; + arrayContains(expression: Expression): BooleanExpression; /** * Creates an expression that checks if an array contains a specific value. @@ -499,12 +538,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param value The element to search for in the array. * @return A new `Expr` representing the 'array_contains' comparison. */ - arrayContains(value: unknown): BooleanExpr; - arrayContains(element: unknown): BooleanExpr { - return new BooleanExpr('array_contains', [ - this, - valueToDefaultExpr(element) - ], 'arrayContains'); + arrayContains(value: unknown): BooleanExpression; + arrayContains(element: unknown): BooleanExpression { + return new BooleanExpression( + 'array_contains', + [this, valueToDefaultExpr(element)], + 'arrayContains' + ); } /** @@ -518,7 +558,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param values The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_all' comparison. */ - arrayContainsAll(values: Array): BooleanExpr; + arrayContainsAll(values: Array): BooleanExpression; /** * Creates an expression that checks if an array contains all the specified elements. @@ -531,12 +571,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param arrayExpression The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_all' comparison. */ - arrayContainsAll(arrayExpression: Expr): BooleanExpr; - arrayContainsAll(values: unknown[] | Expr): BooleanExpr { + arrayContainsAll(arrayExpression: Expression): BooleanExpression; + arrayContainsAll(values: unknown[] | Expression): BooleanExpression { const normalizedExpr = Array.isArray(values) ? new ListOfExprs(values.map(valueToDefaultExpr), 'arrayContainsAll') : values; - return new BooleanExpr('array_contains_all', [this, normalizedExpr], 'arrayContainsAll'); + return new BooleanExpression( + 'array_contains_all', + [this, normalizedExpr], + 'arrayContainsAll' + ); } /** @@ -550,7 +594,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param values The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_any' comparison. */ - arrayContainsAny(values: Array): BooleanExpr; + arrayContainsAny(values: Array): BooleanExpression; /** * Creates an expression that checks if an array contains any of the specified elements. @@ -564,12 +608,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param arrayExpression The elements to check for in the array. * @return A new `Expr` representing the 'array_contains_any' comparison. */ - arrayContainsAny(arrayExpression: Expr): BooleanExpr; - arrayContainsAny(values: Array | Expr): BooleanExpr { + arrayContainsAny(arrayExpression: Expression): BooleanExpression; + arrayContainsAny( + values: Array | Expression + ): BooleanExpression { const normalizedExpr = Array.isArray(values) ? new ListOfExprs(values.map(valueToDefaultExpr), 'arrayContainsAny') : values; - return new BooleanExpr('array_contains_any', [this, normalizedExpr], 'arrayContainsAny'); + return new BooleanExpression( + 'array_contains_any', + [this, normalizedExpr], + 'arrayContainsAny' + ); + } + + /** + * Creates an expression that reverses an array. + * + * ```typescript + * // Reverse the value of the 'myArray' field. + * field("myArray").arrayReverse(); + * ``` + * + * @return A new {@code Expr} representing the reversed array. + */ + arrayReverse(): FunctionExpression { + return new FunctionExpression('array_reverse', [this]); } /** @@ -582,8 +646,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the length of the array. */ - arrayLength(): FunctionExpr { - return new FunctionExpr('array_length', [this], 'arrayLength'); + arrayLength(): FunctionExpression { + return new FunctionExpression('array_length', [this], 'arrayLength'); } /** @@ -592,13 +656,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * field("category").eqAny("Electronics", field("primaryType")); + * field("category").equalAny("Electronics", field("primaryType")); * ``` * * @param values The values or expressions to check against. * @return A new `Expr` representing the 'IN' comparison. */ - eqAny(values: Array): BooleanExpr; + equalAny(values: Array): BooleanExpression; /** * Creates an expression that checks if this expression is equal to any of the provided values or @@ -606,18 +670,18 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * field("category").eqAny(array(["Electronics", field("primaryType")])); + * field("category").equalAny(array(["Electronics", field("primaryType")])); * ``` * * @param arrayExpression An expression that evaluates to an array of values to check against. * @return A new `Expr` representing the 'IN' comparison. */ - eqAny(arrayExpression: Expr): BooleanExpr; - eqAny(others: unknown[] | Expr): BooleanExpr { + equalAny(arrayExpression: Expression): BooleanExpression; + equalAny(others: unknown[] | Expression): BooleanExpression { const exprOthers = Array.isArray(others) - ? new ListOfExprs(others.map(valueToDefaultExpr), 'eqAny') + ? new ListOfExprs(others.map(valueToDefaultExpr), 'equalAny') : others; - return new BooleanExpr('eq_any', [this, exprOthers], 'eqAny'); + return new BooleanExpression('equal_any', [this, exprOthers], 'equalAny'); } /** @@ -626,31 +690,35 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * field("status").notEqAny(["pending", field("rejectedStatus")]); + * field("status").notEqualAny(["pending", field("rejectedStatus")]); * ``` * * @param values The values or expressions to check against. - * @return A new `Expr` representing the 'NotEqAny' comparison. + * @return A new `Expr` representing the 'notEqualAny' comparison. */ - notEqAny(values: Array): BooleanExpr; + notEqualAny(values: Array): BooleanExpression; /** * Creates an expression that checks if this expression is not equal to any of the values in the evaluated expression. * * ```typescript * // Check if the 'status' field is not equal to any value in the field 'rejectedStatuses' - * field("status").notEqAny(field('rejectedStatuses')); + * field("status").notEqualAny(field('rejectedStatuses')); * ``` * * @param arrayExpression The values or expressions to check against. - * @return A new `Expr` representing the 'NotEqAny' comparison. + * @return A new `Expr` representing the 'notEqualAny' comparison. */ - notEqAny(arrayExpression: Expr): BooleanExpr; - notEqAny(others: unknown[] | Expr): BooleanExpr { + notEqualAny(arrayExpression: Expression): BooleanExpression; + notEqualAny(others: unknown[] | Expression): BooleanExpression { const exprOthers = Array.isArray(others) - ? new ListOfExprs(others.map(valueToDefaultExpr), 'notEqAny') + ? new ListOfExprs(others.map(valueToDefaultExpr), 'notEqualAny') : others; - return new BooleanExpr('not_eq_any', [this, exprOthers], 'notEqAny'); + return new BooleanExpression( + 'not_equal_any', + [this, exprOthers], + 'notEqualAny' + ); } /** @@ -663,8 +731,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the 'isNaN' check. */ - isNan(): BooleanExpr { - return new BooleanExpr('is_nan', [this], 'isNan'); + isNan(): BooleanExpression { + return new BooleanExpression('is_nan', [this], 'isNan'); } /** @@ -677,8 +745,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the 'isNull' check. */ - isNull(): BooleanExpr { - return new BooleanExpr('is_null', [this], 'isNull'); + isNull(): BooleanExpression { + return new BooleanExpression('is_null', [this], 'isNull'); } /** @@ -691,8 +759,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the 'exists' check. */ - exists(): BooleanExpr { - return new BooleanExpr('exists', [this], 'exists'); + exists(): BooleanExpression { + return new BooleanExpression('exists', [this], 'exists'); } /** @@ -705,8 +773,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the length of the string. */ - charLength(): FunctionExpr { - return new FunctionExpr('char_length', [this], 'charLength'); + charLength(): FunctionExpression { + return new FunctionExpression('char_length', [this], 'charLength'); } /** @@ -720,7 +788,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @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): BooleanExpr; + like(pattern: string): BooleanExpression; /** * Creates an expression that performs a case-sensitive string comparison. @@ -733,9 +801,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @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): BooleanExpr; - like(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('like', [this, valueToDefaultExpr(stringOrExpr)], 'like'); + like(pattern: Expression): BooleanExpression; + like(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'like', + [this, valueToDefaultExpr(stringOrExpr)], + 'like' + ); } /** @@ -750,7 +822,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The regular expression to use for the search. * @return A new `Expr` representing the 'contains' comparison. */ - regexContains(pattern: string): BooleanExpr; + regexContains(pattern: string): BooleanExpression; /** * Creates an expression that checks if a string contains a specified regular expression as a @@ -764,12 +836,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The regular expression to use for the search. * @return A new `Expr` representing the 'contains' comparison. */ - regexContains(pattern: Expr): BooleanExpr; - regexContains(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('regex_contains', [ - this, - valueToDefaultExpr(stringOrExpr) - ], 'regexContains'); + regexContains(pattern: Expression): BooleanExpression; + regexContains(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'regex_contains', + [this, valueToDefaultExpr(stringOrExpr)], + 'regexContains' + ); } /** @@ -783,7 +856,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The regular expression to use for the match. * @return A new `Expr` representing the regular expression match. */ - regexMatch(pattern: string): BooleanExpr; + regexMatch(pattern: string): BooleanExpression; /** * Creates an expression that checks if a string matches a specified regular expression. @@ -796,12 +869,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param pattern The regular expression to use for the match. * @return A new `Expr` representing the regular expression match. */ - regexMatch(pattern: Expr): BooleanExpr; - regexMatch(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('regex_match', [ - this, - valueToDefaultExpr(stringOrExpr) - ], 'regexMatch'); + regexMatch(pattern: Expression): BooleanExpression; + regexMatch(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'regex_match', + [this, valueToDefaultExpr(stringOrExpr)], + 'regexMatch' + ); } /** @@ -809,31 +883,32 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Check if the 'description' field contains "example". - * field("description").strContains("example"); + * field("description").stringContains("example"); * ``` * * @param substring The substring to search for. * @return A new `Expr` representing the 'contains' comparison. */ - strContains(substring: string): BooleanExpr; + stringContains(substring: string): BooleanExpression; /** * Creates an expression that checks if a string contains the string represented by another expression. * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * field("description").strContains(field("keyword")); + * field("description").stringContains(field("keyword")); * ``` * * @param expr The expression representing the substring to search for. * @return A new `Expr` representing the 'contains' comparison. */ - strContains(expr: Expr): BooleanExpr; - strContains(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('str_contains', [ - this, - valueToDefaultExpr(stringOrExpr) - ], 'strContains'); + stringContains(expr: Expression): BooleanExpression; + stringContains(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'string_contains', + [this, valueToDefaultExpr(stringOrExpr)], + 'stringContains' + ); } /** @@ -847,7 +922,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param prefix The prefix to check for. * @return A new `Expr` representing the 'starts with' comparison. */ - startsWith(prefix: string): BooleanExpr; + startsWith(prefix: string): BooleanExpression; /** * Creates an expression that checks if a string starts with a given prefix (represented as an @@ -861,12 +936,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param prefix The prefix expression to check for. * @return A new `Expr` representing the 'starts with' comparison. */ - startsWith(prefix: Expr): BooleanExpr; - startsWith(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('starts_with', [ - this, - valueToDefaultExpr(stringOrExpr) - ], 'startsWith'); + startsWith(prefix: Expression): BooleanExpression; + startsWith(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'starts_with', + [this, valueToDefaultExpr(stringOrExpr)], + 'startsWith' + ); } /** @@ -880,7 +956,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param suffix The postfix to check for. * @return A new `Expr` representing the 'ends with' comparison. */ - endsWith(suffix: string): BooleanExpr; + endsWith(suffix: string): BooleanExpression; /** * Creates an expression that checks if a string ends with a given postfix (represented as an @@ -894,12 +970,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param suffix The postfix expression to check for. * @return A new `Expr` representing the 'ends with' comparison. */ - endsWith(suffix: Expr): BooleanExpr; - endsWith(stringOrExpr: string | Expr): BooleanExpr { - return new BooleanExpr('ends_with', [ - this, - valueToDefaultExpr(stringOrExpr) - ], 'endsWith'); + endsWith(suffix: Expression): BooleanExpression; + endsWith(stringOrExpr: string | Expression): BooleanExpression { + return new BooleanExpression( + 'ends_with', + [this, valueToDefaultExpr(stringOrExpr)], + 'endsWith' + ); } /** @@ -912,8 +989,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the lowercase string. */ - toLower(): FunctionExpr { - return new FunctionExpr('to_lower', [this], 'toLower'); + toLower(): FunctionExpression { + return new FunctionExpression('to_lower', [this], 'toLower'); } /** @@ -926,8 +1003,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the uppercase string. */ - toUpper(): FunctionExpr { - return new FunctionExpr('to_upper', [this], 'toUpper'); + toUpper(): FunctionExpression { + return new FunctionExpression('to_upper', [this], 'toUpper'); } /** @@ -940,8 +1017,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the trimmed string. */ - trim(): FunctionExpr { - return new FunctionExpr('trim', [this], 'trim'); + trim(): FunctionExpression { + return new FunctionExpression('trim', [this], 'trim'); } /** @@ -949,20 +1026,24 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * field("firstName").strConcat(constant(" "), field("lastName")); + * field("firstName").stringConcat(constant(" "), field("lastName")); * ``` * * @param secondString The additional expression or string literal to concatenate. * @param otherStrings Optional additional expressions or string literals to concatenate. * @return A new `Expr` representing the concatenated string. */ - strConcat( - secondString: Expr | string, - ...otherStrings: Array - ): FunctionExpr { + stringConcat( + secondString: Expression | string, + ...otherStrings: Array + ): FunctionExpression { const elements = [secondString, ...otherStrings]; const exprs = elements.map(valueToDefaultExpr); - return new FunctionExpr('str_concat', [this, ...exprs], 'strConcat'); + return new FunctionExpression( + 'string_concat', + [this, ...exprs], + 'stringConcat' + ); } /** @@ -975,94 +1056,64 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the reversed string. */ - reverse(): FunctionExpr { - return new FunctionExpr('reverse', [this], 'reverse'); + reverse(): FunctionExpression { + return new FunctionExpression('reverse', [this], 'reverse'); } /** - * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring. + * Creates an expression that calculates the length of this string expression in bytes. * * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field - * field("message").replaceFirst("hello", "hi"); + * // Calculate the length of the 'myString' field in bytes. + * field("myString").byteLength(); * ``` * - * @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. + * @return A new {@code Expr} representing the length of the string in bytes. */ - replaceFirst(find: string, replace: string): FunctionExpr; - - /** - * Creates an expression that replaces the first occurrence of a substring within this string expression with another substring, - * where the substring to find and the replacement substring are specified by expressions. - * - * ```typescript - * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field - * field("message").replaceFirst(field("findField"), field("replaceField")); - * ``` - * - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. - */ - replaceFirst(find: Expr, replace: Expr): FunctionExpr; - replaceFirst(find: Expr | string, replace: Expr | string): FunctionExpr { - return new FunctionExpr('replace_first', [ - this, - valueToDefaultExpr(find), - valueToDefaultExpr(replace) - ], 'replaceFirst'); + byteLength(): FunctionExpression { + return new FunctionExpression('byte_length', [this], 'byteLength'); } /** - * Creates an expression that replaces all occurrences of a substring within this string expression with another substring. + * Creates an expression that computes the ceiling of a numeric value. * * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field - * field("message").replaceAll("hello", "hi"); + * // Compute the ceiling of the 'price' field. + * field("price").ceil(); * ``` * - * @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. + * @return A new {@code Expr} representing the ceiling of the numeric value. */ - replaceAll(find: string, replace: string): FunctionExpr; + ceil(): FunctionExpression { + return new FunctionExpression('ceil', [this]); + } /** - * Creates an expression that replaces all occurrences of a substring within this string expression with another substring, - * where the substring to find and the replacement substring are specified by expressions. + * Creates an expression that computes the floor of a numeric value. * * ```typescript - * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field - * field("message").replaceAll(field("findField"), field("replaceField")); + * // Compute the floor of the 'price' field. + * field("price").floor(); * ``` * - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. - */ - replaceAll(find: Expr, replace: Expr): FunctionExpr; - replaceAll(find: Expr | string, replace: Expr | string): FunctionExpr { - return new FunctionExpr('replace_all', [ - this, - valueToDefaultExpr(find), - valueToDefaultExpr(replace) - ], 'replaceAll'); + * @return A new {@code Expr} representing the floor of the numeric value. + */ + floor(): FunctionExpression { + return new FunctionExpression('floor', [this]); } /** - * Creates an expression that calculates the length of this string expression in bytes. + * Creates an expression that computes e to the power of this expression. * * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * field("myString").byteLength(); + * // Compute e to the power of the 'value' field. + * field("value").exp(); * ``` * - * @return A new {@code Expr} representing the length of the string in bytes. + * @return A new {@code Expr} representing the exp of the numeric value. */ - byteLength(): FunctionExpr { - return new FunctionExpr('byte_length', [this], 'byteLength'); + exp(): FunctionExpression { + return new FunctionExpression('exp', [this]); } /** @@ -1076,8 +1127,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param subfield The key to access in the map. * @return A new `Expr` representing the value associated with the given key in the map. */ - mapGet(subfield: string): FunctionExpr { - return new FunctionExpr('map_get', [this, constant(subfield)], 'mapGet'); + mapGet(subfield: string): FunctionExpression { + return new FunctionExpression( + 'map_get', + [this, constant(subfield)], + 'mapGet' + ); } /** @@ -1115,13 +1170,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Calculate the average age of users - * field("age").avg().as("averageAge"); + * field("age").average().as("averageAge"); * ``` * - * @return A new `AggregateFunction` representing the 'avg' aggregation. + * @return A new `AggregateFunction` representing the 'average' aggregation. */ - avg(): AggregateFunction { - return new AggregateFunction('avg', [this], 'avg'); + average(): AggregateFunction { + return new AggregateFunction('average', [this], 'average'); } /** @@ -1132,10 +1187,10 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("price").minimum().as("lowestPrice"); * ``` * - * @return A new `AggregateFunction` representing the 'min' aggregation. + * @return A new `AggregateFunction` representing the 'minimum' aggregation. */ minimum(): AggregateFunction { - return new AggregateFunction('min', [this], 'minimum'); + return new AggregateFunction('minimum', [this], 'minimum'); } /** @@ -1146,10 +1201,24 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * field("score").maximum().as("highestScore"); * ``` * - * @return A new `AggregateFunction` representing the 'max' aggregation. + * @return A new `AggregateFunction` representing the 'maximum' aggregation. */ maximum(): AggregateFunction { - return new AggregateFunction('max', [this], 'maximum'); + return new AggregateFunction('maximum', [this], 'maximum'); + } + + /** + * Creates an aggregation that counts the number of distinct values of the expression or field. + * + * ```typescript + * // Count the distinct number of products + * field("productId").countDistinct().as("distinctProducts"); + * ``` + * + * @return A new `AggregateFunction` representing the 'count_distinct' aggregation. + */ + countDistinct(): AggregateFunction { + return new AggregateFunction('count_distinct', [this], 'countDistinct'); } /** @@ -1162,18 +1231,18 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @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. + * @return A new {@code Expr} representing the logical maximum operation. */ logicalMaximum( - second: Expr | unknown, - ...others: Array - ): FunctionExpr { + second: Expression | unknown, + ...others: Array + ): FunctionExpression { const values = [second, ...others]; - return new FunctionExpr('max', [ - this, - ...values.map(valueToDefaultExpr) - ], - 'logicalMaximum'); + return new FunctionExpression( + 'maximum', + [this, ...values.map(valueToDefaultExpr)], + 'logicalMaximum' + ); } /** @@ -1186,17 +1255,18 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @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. + * @return A new {@code Expr} representing the logical minimum operation. */ logicalMinimum( - second: Expr | unknown, - ...others: Array - ): FunctionExpr { + second: Expression | unknown, + ...others: Array + ): FunctionExpression { const values = [second, ...others]; - return new FunctionExpr('min', [ - this, - ...values.map(valueToDefaultExpr) - ], 'logicalMinimum'); + return new FunctionExpression( + 'minimum', + [this, ...values.map(valueToDefaultExpr)], + 'minimum' + ); } /** @@ -1209,8 +1279,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the length of the vector. */ - vectorLength(): FunctionExpr { - return new FunctionExpr('vector_length', [this], 'vectorLength'); + vectorLength(): FunctionExpression { + return new FunctionExpression('vector_length', [this], 'vectorLength'); } /** @@ -1224,7 +1294,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vectorExpression The other vector (represented as an Expr) to compare against. * @return A new `Expr` representing the cosine distance between the two vectors. */ - cosineDistance(vectorExpression: Expr): FunctionExpr; + cosineDistance(vectorExpression: Expression): FunctionExpression; /** * Calculates the Cosine distance between two vectors. * @@ -1236,9 +1306,15 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vector The other vector (as a VectorValue) to compare against. * @return A new `Expr` representing the Cosine* distance between the two vectors. */ - cosineDistance(vector: VectorValue | number[]): FunctionExpr; - cosineDistance(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('cosine_distance', [this, vectorToExpr(other)], 'cosineDistance'); + cosineDistance(vector: VectorValue | number[]): FunctionExpression; + cosineDistance( + other: Expression | VectorValue | number[] + ): FunctionExpression { + return new FunctionExpression( + 'cosine_distance', + [this, vectorToExpr(other)], + 'cosineDistance' + ); } /** @@ -1252,7 +1328,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vectorExpression The other vector (as an array of numbers) to calculate with. * @return A new `Expr` representing the dot product between the two vectors. */ - dotProduct(vectorExpression: Expr): FunctionExpr; + dotProduct(vectorExpression: Expression): FunctionExpression; /** * Calculates the dot product between two vectors. @@ -1265,9 +1341,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vector The other vector (as an array of numbers) to calculate with. * @return A new `Expr` representing the dot product between the two vectors. */ - dotProduct(vector: VectorValue | number[]): FunctionExpr; - dotProduct(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('dot_product', [this, vectorToExpr(other)], 'dotProduct'); + dotProduct(vector: VectorValue | number[]): FunctionExpression; + dotProduct(other: Expression | VectorValue | number[]): FunctionExpression { + return new FunctionExpression( + 'dot_product', + [this, vectorToExpr(other)], + 'dotProduct' + ); } /** @@ -1281,7 +1361,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vectorExpression The other vector (as an array of numbers) to calculate with. * @return A new `Expr` representing the Euclidean distance between the two vectors. */ - euclideanDistance(vectorExpression: Expr): FunctionExpr; + euclideanDistance(vectorExpression: Expression): FunctionExpression; /** * Calculates the Euclidean distance between two vectors. @@ -1294,9 +1374,15 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param vector The other vector (as a VectorValue) to compare against. * @return A new `Expr` representing the Euclidean distance between the two vectors. */ - euclideanDistance(vector: VectorValue | number[]): FunctionExpr; - euclideanDistance(other: Expr | VectorValue | number[]): FunctionExpr { - return new FunctionExpr('euclidean_distance', [this, vectorToExpr(other)], 'euclideanDistance'); + euclideanDistance(vector: VectorValue | number[]): FunctionExpression; + euclideanDistance( + other: Expression | VectorValue | number[] + ): FunctionExpression { + return new FunctionExpression( + 'euclidean_distance', + [this, vectorToExpr(other)], + 'euclideanDistance' + ); } /** @@ -1310,8 +1396,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the timestamp. */ - unixMicrosToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_micros_to_timestamp', [this], 'unixMicrosToTimestamp'); + unixMicrosToTimestamp(): FunctionExpression { + return new FunctionExpression( + 'unix_micros_to_timestamp', + [this], + 'unixMicrosToTimestamp' + ); } /** @@ -1324,8 +1414,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the number of microseconds since epoch. */ - timestampToUnixMicros(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_micros', [this], 'timestampToUnixMicros'); + timestampToUnixMicros(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_micros', + [this], + 'timestampToUnixMicros' + ); } /** @@ -1339,8 +1433,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the timestamp. */ - unixMillisToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_millis_to_timestamp', [this], 'unixMillisToTimestamp'); + unixMillisToTimestamp(): FunctionExpression { + return new FunctionExpression( + 'unix_millis_to_timestamp', + [this], + 'unixMillisToTimestamp' + ); } /** @@ -1353,8 +1451,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the number of milliseconds since epoch. */ - timestampToUnixMillis(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_millis', [this], 'timestampToUnixMillis'); + timestampToUnixMillis(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_millis', + [this], + 'timestampToUnixMillis' + ); } /** @@ -1368,8 +1470,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the timestamp. */ - unixSecondsToTimestamp(): FunctionExpr { - return new FunctionExpr('unix_seconds_to_timestamp', [this], 'unixSecondsToTimestamp'); + unixSecondsToTimestamp(): FunctionExpression { + return new FunctionExpression( + 'unix_seconds_to_timestamp', + [this], + 'unixSecondsToTimestamp' + ); } /** @@ -1382,8 +1488,12 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the number of seconds since epoch. */ - timestampToUnixSeconds(): FunctionExpr { - return new FunctionExpr('timestamp_to_unix_seconds', [this], 'timestampToUnixSeconds'); + timestampToUnixSeconds(): FunctionExpression { + return new FunctionExpression( + 'timestamp_to_unix_seconds', + [this], + 'timestampToUnixSeconds' + ); } /** @@ -1398,7 +1508,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param amount The expression evaluates to amount of the unit. * @return A new {@code Expr} representing the resulting timestamp. */ - timestampAdd(unit: Expr, amount: Expr): FunctionExpr; + timestampAdd(unit: Expression, amount: Expression): FunctionExpression; /** * Creates an expression that adds a specified amount of time to this timestamp expression. @@ -1415,23 +1525,23 @@ export abstract class Expr implements ProtoValueSerializable, UserData { timestampAdd( unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number - ): FunctionExpr; + ): FunctionExpression; timestampAdd( unit: - | Expr + | Expression | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: Expr | number - ): FunctionExpr { - return new FunctionExpr('timestamp_add', [ - this, - valueToDefaultExpr(unit), - valueToDefaultExpr(amount) - ], 'timestampAdd'); + amount: Expression | number + ): FunctionExpression { + return new FunctionExpression( + 'timestamp_add', + [this, valueToDefaultExpr(unit), valueToDefaultExpr(amount)], + 'timestampAdd' + ); } /** @@ -1439,238 +1549,47 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * ```typescript * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. - * field("timestamp").timestampSub(field("unit"), field("amount")); + * field("timestamp").timestampSubtract(field("unit"), field("amount")); * ``` * * @param unit The expression evaluates to unit of time, must be one of 'microsecond', 'millisecond', 'second', 'minute', 'hour', 'day'. * @param amount The expression evaluates to amount of the unit. * @return A new {@code Expr} representing the resulting timestamp. */ - timestampSub(unit: Expr, amount: Expr): FunctionExpr; + timestampSubtract(unit: Expression, amount: Expression): FunctionExpression; /** * Creates an expression that subtracts a specified amount of time from this timestamp expression. * * ```typescript * // Subtract 1 day from the 'timestamp' field. - * field("timestamp").timestampSub("day", 1); + * field("timestamp").timestampSubtract("day", 1); * ``` * * @param unit The unit of time to subtract (e.g., "day", "hour"). * @param amount The amount of time to subtract. * @return A new {@code Expr} representing the resulting timestamp. */ - timestampSub( + timestampSubtract( unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number - ): FunctionExpr; - timestampSub( + ): FunctionExpression; + timestampSubtract( unit: - | Expr + | Expression | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: Expr | number - ): FunctionExpr { - return new FunctionExpr('timestamp_sub', [ - this, - valueToDefaultExpr(unit), - valueToDefaultExpr(amount) - ], 'timestampSub'); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise AND operation between this expression and a constant. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 0xFF. - * field("field1").bitAnd(0xFF); - * ``` - * - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise AND operation. - */ - bitAnd(otherBits: number | Bytes): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise AND operation between two expressions. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 'field2'. - * field("field1").bitAnd(field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise AND operation. - */ - bitAnd(bitsExpression: Expr): FunctionExpr; - bitAnd(bitsOrExpression: number | Expr | Bytes): FunctionExpr { - return new FunctionExpr('bit_and', [ - this, - valueToDefaultExpr(bitsOrExpression) - ], 'bitAnd'); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise OR operation between this expression and a constant. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 0xFF. - * field("field1").bitOr(0xFF); - * ``` - * - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise OR operation. - */ - bitOr(otherBits: number | Bytes): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise OR operation between two expressions. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 'field2'. - * field("field1").bitOr(field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise OR operation. - */ - bitOr(bitsExpression: Expr): FunctionExpr; - bitOr(bitsOrExpression: number | Expr | Bytes): FunctionExpr { - return new FunctionExpr('bit_or', [ - this, - valueToDefaultExpr(bitsOrExpression) - ], 'bitOr'); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between this expression and a constant. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 0xFF. - * field("field1").bitXor(0xFF); - * ``` - * - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ - bitXor(otherBits: number | Bytes): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between two expressions. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 'field2'. - * field("field1").bitXor(field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ - bitXor(bitsExpression: Expr): FunctionExpr; - bitXor(bitsOrExpression: number | Expr | Bytes): FunctionExpr { - return new FunctionExpr('bit_xor', [ - this, - valueToDefaultExpr(bitsOrExpression) - ], 'bitXor'); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise NOT operation to this expression. - * - * ```typescript - * // Calculate the bitwise NOT of 'field1'. - * field("field1").bitNot(); - * ``` - * - * @return A new {@code Expr} representing the bitwise NOT operation. - */ - bitNot(): FunctionExpr { - return new FunctionExpr('bit_not', [this], 'bitNot'); - } - - /** - * @beta - * - * Creates an expression that applies a bitwise left shift operation to this expression. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 2 bits. - * field("field1").bitLeftShift(2); - * ``` - * - * @param y The operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ - bitLeftShift(y: number): FunctionExpr; - /** - * @beta - * - * Creates an expression that applies a bitwise left shift operation to this expression. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 'field2' bits. - * field("field1").bitLeftShift(field("field2")); - * ``` - * - * @param numberExpr The operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ - bitLeftShift(numberExpr: Expr): FunctionExpr; - bitLeftShift(numberExpr: number | Expr): FunctionExpr { - return new FunctionExpr('bit_left_shift', [ - this, - valueToDefaultExpr(numberExpr) - ], 'bitLeftShift'); - } - - /** - * @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) - ], 'bitRightShift'); + amount: Expression | number + ): FunctionExpression { + return new FunctionExpression( + 'timestamp_subtract', + [this, valueToDefaultExpr(unit), valueToDefaultExpr(amount)], + 'timestampSubtract' + ); } /** @@ -1685,8 +1604,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the documentId operation. */ - documentId(): FunctionExpr { - return new FunctionExpr('document_id', [this], 'documentId'); + documentId(): FunctionExpression { + return new FunctionExpression('document_id', [this], 'documentId'); } /** @@ -1698,7 +1617,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @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; + substring(position: number, length?: number): FunctionExpression; /** * @beta @@ -1709,17 +1628,24 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @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 { + substring(position: Expression, length?: Expression): FunctionExpression; + substring( + position: Expression | number, + length?: Expression | number + ): FunctionExpression { const positionExpr = valueToDefaultExpr(position); if (length === undefined) { - return new FunctionExpr('substr', [this, positionExpr], 'substr'); + return new FunctionExpression( + 'substring', + [this, positionExpr], + 'substring' + ); } else { - return new FunctionExpr('substr', [ - this, - positionExpr, - valueToDefaultExpr(length) - ], 'substr'); + return new FunctionExpression( + 'substring', + [this, positionExpr, valueToDefaultExpr(length)], + 'substring' + ); } } @@ -1737,7 +1663,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param offset The index of the element to return. * @return A new Expr representing the 'arrayGet' operation. */ - arrayGet(offset: number): FunctionExpr; + arrayGet(offset: number): FunctionExpression; /** * @beta @@ -1754,9 +1680,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @param offsetExpr An Expr evaluating to the index of the element to return. * @return A new Expr representing the 'arrayGet' operation. */ - arrayGet(offsetExpr: Expr): FunctionExpr; - arrayGet(offset: Expr | number): FunctionExpr { - return new FunctionExpr('array_get', [this, valueToDefaultExpr(offset)], 'arrayGet'); + arrayGet(offsetExpr: Expression): FunctionExpression; + arrayGet(offset: Expression | number): FunctionExpression { + return new FunctionExpression( + 'array_get', + [this, valueToDefaultExpr(offset)], + 'arrayGet' + ); } /** @@ -1771,8 +1701,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code BooleanExpr} representing the 'isError' check. */ - isError(): BooleanExpr { - return new BooleanExpr('is_error', [this], 'isError'); + isError(): BooleanExpression { + return new BooleanExpression('is_error', [this], 'isError'); } /** @@ -1791,7 +1721,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * returned if this expression produces an error. * @return A new {@code Expr} representing the 'ifError' operation. */ - ifError(catchExpr: Expr): FunctionExpr; + ifError(catchExpr: Expression): FunctionExpression; /** * @beta @@ -1809,9 +1739,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * produces an error. * @return A new {@code Expr} representing the 'ifError' operation. */ - ifError(catchValue: unknown): FunctionExpr; - ifError(catchValue: unknown): FunctionExpr { - return new FunctionExpr('if_error', [this, valueToDefaultExpr(catchValue)], 'ifError'); + ifError(catchValue: unknown): FunctionExpression; + ifError(catchValue: unknown): FunctionExpression { + return new FunctionExpression( + 'if_error', + [this, valueToDefaultExpr(catchValue)], + 'ifError' + ); } /** @@ -1827,8 +1761,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code BooleanExpr} representing the 'isAbsent' check. */ - isAbsent(): BooleanExpr { - return new BooleanExpr('is_absent', [this], 'isAbsent'); + isAbsent(): BooleanExpression { + return new BooleanExpression('is_absent', [this], 'isAbsent'); } /** @@ -1843,8 +1777,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code BooleanExpr} representing the 'isNotNull' check. */ - isNotNull(): BooleanExpr { - return new BooleanExpr('is_not_null', [this], 'isNotNull'); + isNotNull(): BooleanExpression { + return new BooleanExpression('is_not_null', [this], 'isNotNull'); } /** @@ -1859,8 +1793,8 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * * @return A new {@code Expr} representing the 'isNaN' check. */ - isNotNan(): BooleanExpr { - return new BooleanExpr('is_not_nan', [this], 'isNotNan'); + isNotNan(): BooleanExpression { + return new BooleanExpression('is_not_nan', [this], 'isNotNan'); } /** @@ -1876,7 +1810,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @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; + mapRemove(key: string): FunctionExpression; /** * @beta * @@ -1890,12 +1824,13 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @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) - ], 'mapRemove'); + mapRemove(keyExpr: Expression): FunctionExpression; + mapRemove(stringExpr: Expression | string): FunctionExpression { + return new FunctionExpression( + 'map_remove', + [this, valueToDefaultExpr(stringExpr)], + 'mapRemove' + ); } /** @@ -1906,7 +1841,7 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ``` * // 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}, {}) + * field('settings').mapMerge({ enabled: true }, conditional(field('isAdmin'), { admin: true}, {}) * ``` * * @param secondMap A required second map to merge. Represented as a literal or @@ -1917,18 +1852,165 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * @returns A new {@code FirestoreFunction} representing the 'mapMerge' operation. */ mapMerge( - secondMap: Record | Expr, - ...otherMaps: Array | Expr> - ): FunctionExpr { + secondMap: Record | Expression, + ...otherMaps: Array | Expression> + ): FunctionExpression { const secondMapExpr = valueToDefaultExpr(secondMap); const otherMapExprs = otherMaps.map(valueToDefaultExpr); - return new FunctionExpr('map_merge', [ - this, - secondMapExpr, - ...otherMapExprs - ], 'mapMerge'); + return new FunctionExpression( + 'map_merge', + [this, secondMapExpr, ...otherMapExprs], + 'mapMerge' + ); + } + + /** + * Creates an expression that returns the value of this expression raised to the power of another expression. + * + * ```typescript + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * field("base").pow(field("exponent")); + * ``` + * + * @param exponent The expression to raise this expression to the power of. + * @return A new `Expr` representing the power operation. + */ + pow(exponent: Expression): FunctionExpression; + + /** + * Creates an expression that returns the value of this expression raised to the power of a constant value. + * + * ```typescript + * // Raise the value of the 'base' field to the power of 2. + * field("base").pow(2); + * ``` + * + * @param exponent The constant value to raise this expression to the power of. + * @return A new `Expr` representing the power operation. + */ + pow(exponent: number): FunctionExpression; + pow(exponent: number | Expression): FunctionExpression { + return new FunctionExpression('pow', [this, valueToDefaultExpr(exponent)]); + } + + /** + * Creates an expression that rounds a numeric value to the nearest whole number. + * + * ```typescript + * // Round the value of the 'price' field. + * field("price").round(); + * ``` + * + * @return A new `Expr` representing the rounded value. + */ + round(): FunctionExpression { + return new FunctionExpression('round', [this]); + } + + /** + * Creates an expression that returns the collection ID from a path. + * + * ```typescript + * // Get the collection ID from a path. + * field("__path__").collectionId(); + * ``` + * + * @return A new {@code Expr} representing the collectionId operation. + */ + collectionId(): FunctionExpression { + return new FunctionExpression('collection_id', [this]); + } + + /** + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * field("name").length(); + * + * // Get the number of items in the 'cart' array. + * field("cart").length(); + * ``` + * + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ + length(): FunctionExpression { + return new FunctionExpression('length', [this]); + } + + /** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * field("value").ln(); + * ``` + * + * @return A new {@code Expr} representing the natural logarithm of the numeric value. + */ + ln(): FunctionExpression { + return new FunctionExpression('ln', [this]); } + /** + * Creates an expression that computes the logarithm of this expression to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with base 10. + * field("value").log(10); + * ``` + * + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ + log(base: number): FunctionExpression; + + /** + * Creates an expression that computes the logarithm of this expression to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with the base in the 'base' field. + * field("value").log(field("base")); + * ``` + * + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ + log(base: Expression): FunctionExpression; + log(base: number | Expression): FunctionExpression { + return new FunctionExpression('log', [this, valueToDefaultExpr(base)]); + } + + /** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * field("value").sqrt(); + * ``` + * + * @return A new {@code Expr} representing the square root of the numeric value. + */ + sqrt(): FunctionExpression { + return new FunctionExpression('sqrt', [this]); + } + + /** + * Creates an expression that reverses a string. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * field("myString").stringReverse(); + * ``` + * + * @return A new {@code Expr} representing the reversed string. + */ + stringReverse(): FunctionExpression { + return new FunctionExpression('string_reverse', [this]); + } + + // TODO(new-expression): Add new expression method definitions above this line + /** * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. * @@ -1972,11 +2054,11 @@ export abstract class Expr implements ProtoValueSerializable, UserData { * ``` * * @param name The alias to assign to this expression. - * @return A new {@link ExprWithAlias} that wraps this + * @return A new {@link AliasedExpression} that wraps this * expression and associates it with the provided alias. */ - as(name: string): ExprWithAlias { - return new ExprWithAlias(this, name, 'as'); + as(name: string): AliasedExpression { + return new AliasedExpression(this, name, 'as'); } } @@ -1987,8 +2069,16 @@ export abstract class Expr implements ProtoValueSerializable, UserData { */ export interface Selectable { selectable: true; + /** + * @private + * @internal + */ readonly alias: string; - readonly expr: Expr; + /** + * @private + * @internal + */ + readonly expr: Expression; } /** @@ -1997,11 +2087,26 @@ export interface Selectable { * A class that represents an aggregate function. */ export class AggregateFunction implements ProtoValueSerializable, UserData { - exprType: ExprType = 'AggregateFunction'; + exprType: ExpressionType = 'AggregateFunction'; - constructor( name: string, params: Expr[]); - constructor( name: string, params: Expr[], _methodName: string | undefined); - constructor(private name: string, private params: Expr[], readonly _methodName?: string) {} + constructor(name: string, params: Expression[]); + /** + * INTERNAL Constructor with method name for validation. + * @hideconstructor + * @param name + * @param params + * @param _methodName + */ + constructor( + name: string, + params: Expression[], + _methodName: string | undefined + ); + constructor( + private name: string, + private params: Expression[], + readonly _methodName?: string + ) {} /** * Assigns an alias to this AggregateFunction. The alias specifies the name that @@ -2010,7 +2115,7 @@ export class AggregateFunction implements ProtoValueSerializable, UserData { * ```typescript * // Calculate the average price of all items and assign it the alias "averagePrice". * firestore.pipeline().collection("items") - * .aggregate(field("price").avg().as("averagePrice")); + * .aggregate(field("price").average().as("averagePrice")); * ``` * * @param name The alias to assign to this AggregateFunction. @@ -2041,7 +2146,9 @@ export class AggregateFunction implements ProtoValueSerializable, UserData { * @internal */ _readUserData(context: ParseContext): void { - context = this._methodName ? context.contextWith({methodName: this._methodName}) : context; + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; this.params.forEach(expr => { return expr._readUserData(context); }); @@ -2054,7 +2161,11 @@ export class AggregateFunction implements ProtoValueSerializable, UserData { * An AggregateFunction with alias. */ export class AggregateWithAlias implements UserData { - constructor(readonly aggregate: AggregateFunction, readonly alias: string, readonly _methodName: string | undefined) {} + constructor( + readonly aggregate: AggregateFunction, + readonly alias: string, + readonly _methodName: string | undefined + ) {} /** * @internal @@ -2076,8 +2187,8 @@ export class AggregateWithAlias implements UserData { /** * @beta */ -export class ExprWithAlias implements Selectable, UserData { - exprType: ExprType = 'ExprWithAlias'; +export class AliasedExpression implements Selectable, UserData { + exprType: ExpressionType = 'AliasedExpression'; selectable = true as const; /** @@ -2088,7 +2199,11 @@ export class ExprWithAlias implements Selectable, UserData { */ _createdFromLiteral: boolean = false; - constructor(readonly expr: Expr, readonly alias: string, readonly _methodName: string | undefined) {} + constructor( + readonly expr: Expression, + readonly alias: string, + readonly _methodName: string | undefined + ) {} /** * @private @@ -2102,10 +2217,13 @@ export class ExprWithAlias implements Selectable, UserData { /** * @internal */ -class ListOfExprs extends Expr implements UserData { - exprType: ExprType = 'ListOfExprs'; +class ListOfExprs extends Expression implements UserData { + expressionType: ExpressionType = 'ListOfExpressions'; - constructor(private exprs: Expr[], readonly _methodName: string | undefined) { + constructor( + private exprs: Expression[], + readonly _methodName: string | undefined + ) { super(); } @@ -2126,7 +2244,7 @@ class ListOfExprs extends Expr implements UserData { * @internal */ _readUserData(context: ParseContext): void { - this.exprs.forEach((expr: Expr) => expr._readUserData(context)); + this.exprs.forEach((expr: Expression) => expr._readUserData(context)); } } @@ -2148,8 +2266,8 @@ class ListOfExprs extends Expr implements UserData { * const cityField = field("address.city"); * ``` */ -export class Field extends Expr implements Selectable { - readonly exprType: ExprType = 'Field'; +export class Field extends Expression implements Selectable { + readonly expressionType: ExpressionType = 'Field'; selectable = true as const; /** @@ -2158,19 +2276,22 @@ export class Field extends Expr implements Selectable { * @hideconstructor * @param fieldPath */ - constructor(private fieldPath: InternalFieldPath, readonly _methodName: string | undefined) { + constructor( + private fieldPath: InternalFieldPath, + readonly _methodName: string | undefined + ) { super(); } - fieldName(): string { + get fieldName(): string { return this.fieldPath.canonicalString(); } get alias(): string { - return this.fieldName(); + return this.fieldName; } - get expr(): Expr { + get expr(): Expression { return this; } @@ -2214,7 +2335,10 @@ export function field(nameOrPath: string | FieldPath): Field { return _field(nameOrPath, 'field'); } -export function _field(nameOrPath: string | FieldPath, methodName: string | undefined): Field { +export function _field( + nameOrPath: string | FieldPath, + methodName: string | undefined +): Field { if (typeof nameOrPath === 'string') { if (DOCUMENT_KEY_NAME === nameOrPath) { return new Field(documentIdFieldPath()._internalPath, methodName); @@ -2226,7 +2350,7 @@ export function _field(nameOrPath: string | FieldPath, methodName: string | unde } /** - * @beta + * @internal * * Represents a constant value that can be used in a Firestore pipeline expression. * @@ -2240,8 +2364,8 @@ export function _field(nameOrPath: string | FieldPath, methodName: string | unde * const hello = constant("hello"); * ``` */ -export class Constant extends Expr { - readonly exprType: ExprType = 'Constant'; +export class Constant extends Expression { + readonly expressionType: ExpressionType = 'Constant'; private _protoValue?: ProtoValue; @@ -2251,7 +2375,10 @@ export class Constant extends Expr { * @hideconstructor * @param value The value of the constant. */ - constructor(private value: unknown, readonly _methodName: string | undefined) { + constructor( + private value: unknown, + readonly _methodName: string | undefined + ) { super(); } @@ -2283,7 +2410,9 @@ export class Constant extends Expr { * @internal */ _readUserData(context: ParseContext): void { - context = this._methodName ? context.contextWith({methodName: this._methodName}) : context; + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; if (isFirestoreValue(this._protoValue)) { return; } else { @@ -2298,7 +2427,7 @@ export class Constant extends Expr { * @param value The number value. * @return A new `Constant` instance. */ -export function constant(value: number): Constant; +export function constant(value: number): Expression; /** * Creates a `Constant` instance for a string value. @@ -2306,7 +2435,7 @@ export function constant(value: number): Constant; * @param value The string value. * @return A new `Constant` instance. */ -export function constant(value: string): Constant; +export function constant(value: string): Expression; /** * Creates a `Constant` instance for a boolean value. @@ -2314,7 +2443,7 @@ export function constant(value: string): Constant; * @param value The boolean value. * @return A new `Constant` instance. */ -export function constant(value: boolean): Constant; +export function constant(value: boolean): Expression; /** * Creates a `Constant` instance for a null value. @@ -2322,7 +2451,7 @@ export function constant(value: boolean): Constant; * @param value The null value. * @return A new `Constant` instance. */ -export function constant(value: null): Constant; +export function constant(value: null): Expression; /** * Creates a `Constant` instance for a GeoPoint value. @@ -2330,7 +2459,7 @@ export function constant(value: null): Constant; * @param value The GeoPoint value. * @return A new `Constant` instance. */ -export function constant(value: GeoPoint): Constant; +export function constant(value: GeoPoint): Expression; /** * Creates a `Constant` instance for a Timestamp value. @@ -2338,7 +2467,7 @@ export function constant(value: GeoPoint): Constant; * @param value The Timestamp value. * @return A new `Constant` instance. */ -export function constant(value: Timestamp): Constant; +export function constant(value: Timestamp): Expression; /** * Creates a `Constant` instance for a Date value. @@ -2346,7 +2475,7 @@ export function constant(value: Timestamp): Constant; * @param value The Date value. * @return A new `Constant` instance. */ -export function constant(value: Date): Constant; +export function constant(value: Date): Expression; /** * Creates a `Constant` instance for a Bytes value. @@ -2354,7 +2483,7 @@ export function constant(value: Date): Constant; * @param value The Bytes value. * @return A new `Constant` instance. */ -export function constant(value: Bytes): Constant; +export function constant(value: Bytes): Expression; /** * Creates a `Constant` instance for a DocumentReference value. @@ -2362,7 +2491,7 @@ export function constant(value: Bytes): Constant; * @param value The DocumentReference value. * @return A new `Constant` instance. */ -export function constant(value: DocumentReference): Constant; +export function constant(value: DocumentReference): Expression; /** * Creates a `Constant` instance for a Firestore proto value. @@ -2372,7 +2501,7 @@ export function constant(value: DocumentReference): Constant; * @param value The Firestore proto value. * @return A new `Constant` instance. */ -export function constant(value: ProtoValue): Constant; +export function constant(value: ProtoValue): Expression; /** * Creates a `Constant` instance for a VectorValue value. @@ -2380,9 +2509,9 @@ export function constant(value: ProtoValue): Constant; * @param value The VectorValue value. * @return A new `Constant` instance. */ -export function constant(value: VectorValue): Constant; +export function constant(value: VectorValue): Expression; -export function constant(value: unknown): Constant { +export function constant(value: unknown): Expression { return _constant(value, 'contant'); } @@ -2392,44 +2521,32 @@ export function constant(value: unknown): Constant { * @param value * @param methodName */ -export function _constant(value: unknown, methodName: string | undefined): Constant { +export function _constant( + value: unknown, + methodName: string | undefined +): Constant { return new Constant(value, methodName); } -/** - * TODO remove - * Creates a `Constant` instance for a VectorValue value. - * - * ```typescript - * // Create a Constant instance for a vector value - * const vectorConstant = constantVector([1, 2, 3]); - * ``` - * - * @param value The VectorValue value. - * @return A new `Constant` instance. - */ -export function constantVector(value: number[] | VectorValue): Constant { - if (value instanceof VectorValue) { - return _constant(value, 'constantVector'); - } else { - return _constant(new VectorValue(value as number[]), 'constantVector'); - } -} - /** * Internal only * @internal * @private */ -export class MapValue extends Expr { - constructor(private plainObject: Map, readonly _methodName: string | undefined) { +export class MapValue extends Expression { + constructor( + private plainObject: Map, + readonly _methodName: string | undefined + ) { super(); } - exprType: ExprType = 'Constant'; + expressionType: ExpressionType = 'Constant'; _readUserData(context: ParseContext): void { - context = this._methodName ? context.contextWith({methodName: this._methodName}) : context; + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; this.plainObject.forEach(expr => { expr._readUserData(context); }); @@ -2446,15 +2563,23 @@ export class MapValue extends Expr { * 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. + * Typically, you would not use this class or its children directly. Use either the functions like {@link and}, {@link equal}, + * or the methods on {@link Expression} ({@link Expression#equal}, {@link Expression#lessThan}, etc.) to construct new Function instances. */ -export class FunctionExpr extends Expr { - readonly exprType: ExprType = 'Function'; +export class FunctionExpression extends Expression { + readonly expressionType: ExpressionType = 'Function'; - constructor( name: string, params: Expr[]); - constructor( name: string, params: Expr[], _methodName: string | undefined); - constructor(private name: string, private params: Expr[], readonly _methodName?: string) { + constructor(name: string, params: Expression[]); + constructor( + name: string, + params: Expression[], + _methodName: string | undefined + ); + constructor( + private name: string, + private params: Expression[], + readonly _methodName?: string + ) { super(); } @@ -2476,7 +2601,9 @@ export class FunctionExpr extends Expr { * @internal */ _readUserData(context: ParseContext): void { - context = this._methodName ? context.contextWith({methodName: this._methodName}) : context; + context = this._methodName + ? context.contextWith({ methodName: this._methodName }) + : context; this.params.forEach(expr => { return expr._readUserData(context); }); @@ -2488,7 +2615,7 @@ export class FunctionExpr extends Expr { * * An interface that represents a filter condition. */ -export class BooleanExpr extends FunctionExpr { +export class BooleanExpression extends FunctionExpression { filterable: true = true; /** @@ -2497,7 +2624,7 @@ export class BooleanExpr extends FunctionExpr { * * ```typescript * // Find the count of documents with a score greater than 90 - * field("score").gt(90).countIf().as("highestScore"); + * field("score").greaterThan(90).countIf().as("highestScore"); * ``` * * @return A new `AggregateFunction` representing the 'countIf' aggregation. @@ -2516,8 +2643,8 @@ export class BooleanExpr extends FunctionExpr { * * @return A new {@code Expr} representing the negated filter condition. */ - not(): BooleanExpr { - return new BooleanExpr('not', [this], 'not'); + not(): BooleanExpression { + return new BooleanExpression('not', [this], 'not'); } } @@ -2528,414 +2655,16 @@ export class BooleanExpr extends FunctionExpr { * * ```typescript * // Count the number of documents where 'is_active' field equals true - * countIf(field("is_active").eq(true)).as("numActiveDocuments"); + * countIf(field("is_active").equal(true)).as("numActiveDocuments"); * ``` * * @param booleanExpr - The boolean expression to evaluate on each input. * @returns A new `AggregateFunction` representing the 'countIf' aggregation. */ -export function countIf(booleanExpr: BooleanExpr): AggregateFunction { +export function countIf(booleanExpr: BooleanExpression): AggregateFunction { return booleanExpr.countIf(); } -/** - * @beta - * Creates an expression that return a pseudo-random value of type double in the - * range of [0, 1), inclusive of 0 and exclusive of 1. - * - * @returns A new `Expr` representing the 'rand' function. - */ -export function rand(): FunctionExpr { - return new FunctionExpr('rand', [], 'rand'); -} - -/** - * @beta - * - * Creates an expression that applies a bitwise AND operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 0xFF. - * bitAnd("field1", 0xFF); - * ``` - * - * @param field The left operand field name. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise AND operation. - */ -export function bitAnd(field: string, otherBits: number | Bytes): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise AND operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 'field2'. - * bitAnd("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise AND operation. - */ -export function bitAnd(field: string, bitsExpression: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise AND operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 0xFF. - * bitAnd(field("field1"), 0xFF); - * ``` - * - * @param bitsExpression An expression returning bits. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise AND operation. - */ -export function bitAnd( - bitsExpression: Expr, - otherBits: number | Bytes -): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise AND operation between two expressions. - * - * ```typescript - * // Calculate the bitwise AND of 'field1' and 'field2'. - * bitAnd(field("field1"), field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @param otherBitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise AND operation. - */ -export function bitAnd( - bitsExpression: Expr, - otherBitsExpression: Expr -): FunctionExpr; -export function bitAnd( - bits: string | Expr, - bitsOrExpression: number | Expr | Bytes -): FunctionExpr { - return fieldOrExpression(bits).bitAnd(valueToDefaultExpr(bitsOrExpression)); -} - -/** - * @beta - * - * Creates an expression that applies a bitwise OR operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 0xFF. - * bitOr("field1", 0xFF); - * ``` - * - * @param field The left operand field name. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise OR operation. - */ -export function bitOr(field: string, otherBits: number | Bytes): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise OR operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 'field2'. - * bitOr("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise OR operation. - */ -export function bitOr(field: string, bitsExpression: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise OR operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 0xFF. - * bitOr(field("field1"), 0xFF); - * ``` - * - * @param bitsExpression An expression returning bits. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise OR operation. - */ -export function bitOr( - bitsExpression: Expr, - otherBits: number | Bytes -): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise OR operation between two expressions. - * - * ```typescript - * // Calculate the bitwise OR of 'field1' and 'field2'. - * bitOr(field("field1"), field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @param otherBitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise OR operation. - */ -export function bitOr( - bitsExpression: Expr, - otherBitsExpression: Expr -): FunctionExpr; -export function bitOr( - bits: string | Expr, - bitsOrExpression: number | Expr | Bytes -): FunctionExpr { - return fieldOrExpression(bits).bitOr(valueToDefaultExpr(bitsOrExpression)); -} - -/** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 0xFF. - * bitXor("field1", 0xFF); - * ``` - * - * @param field The left operand field name. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ -export function bitXor(field: string, otherBits: number | Bytes): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 'field2'. - * bitXor("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param bitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ -export function bitXor(field: string, bitsExpression: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 0xFF. - * bitXor(field("field1"), 0xFF); - * ``` - * - * @param bitsExpression An expression returning bits. - * @param otherBits A constant representing bits. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ -export function bitXor( - bitsExpression: Expr, - otherBits: number | Bytes -): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise XOR operation between two expressions. - * - * ```typescript - * // Calculate the bitwise XOR of 'field1' and 'field2'. - * bitXor(field("field1"), field("field2")); - * ``` - * - * @param bitsExpression An expression that returns bits when evaluated. - * @param otherBitsExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise XOR operation. - */ -export function bitXor( - bitsExpression: Expr, - otherBitsExpression: Expr -): FunctionExpr; -export function bitXor( - bits: string | Expr, - bitsOrExpression: number | Expr | Bytes -): FunctionExpr { - return fieldOrExpression(bits).bitXor(valueToDefaultExpr(bitsOrExpression)); -} - -/** - * @beta - * - * Creates an expression that applies a bitwise NOT operation to a field. - * - * ```typescript - * // Calculate the bitwise NOT of 'field1'. - * bitNot("field1"); - * ``` - * - * @param field The operand field name. - * @return A new {@code Expr} representing the bitwise NOT operation. - */ -export function bitNot(field: string): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise NOT operation to an expression. - * - * ```typescript - * // Calculate the bitwise NOT of 'field1'. - * bitNot(field("field1")); - * ``` - * - * @param bitsValueExpression An expression that returns bits when evaluated. - * @return A new {@code Expr} representing the bitwise NOT operation. - */ -export function bitNot(bitsValueExpression: Expr): FunctionExpr; -export function bitNot(bits: string | Expr): FunctionExpr { - return fieldOrExpression(bits).bitNot(); -} - -/** - * @beta - * - * Creates an expression that applies a bitwise left shift operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 2 bits. - * bitLeftShift("field1", 2); - * ``` - * - * @param field The left operand field name. - * @param y The right operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ -export function bitLeftShift(field: string, y: number): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise left shift operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 'field2' bits. - * bitLeftShift("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param numberExpr The right operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ -export function bitLeftShift(field: string, numberExpr: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise left shift operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 2 bits. - * bitLeftShift(field("field1"), 2); - * ``` - * - * @param xValue An expression returning bits. - * @param y The right operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ -export function bitLeftShift(xValue: Expr, y: number): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise left shift operation between two expressions. - * - * ```typescript - * // Calculate the bitwise left shift of 'field1' by 'field2' bits. - * bitLeftShift(field("field1"), field("field2")); - * ``` - * - * @param xValue An expression returning bits. - * @param numberExpr The right operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise left shift operation. - */ -export function bitLeftShift(xValue: Expr, numberExpr: Expr): FunctionExpr; -export function bitLeftShift( - xValue: string | Expr, - numberExpr: number | Expr -): FunctionExpr { - return fieldOrExpression(xValue).bitLeftShift(valueToDefaultExpr(numberExpr)); -} - -/** - * @beta - * - * Creates an expression that applies a bitwise right shift operation between a field and a constant. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 2 bits. - * bitRightShift("field1", 2); - * ``` - * - * @param field The left operand field name. - * @param y The right operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ -export function bitRightShift(field: string, y: number): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise right shift operation between a field and an expression. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 'field2' bits. - * bitRightShift("field1", field("field2")); - * ``` - * - * @param field The left operand field name. - * @param numberExpr The right operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ -export function bitRightShift(field: string, numberExpr: Expr): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise right shift operation between an expression and a constant. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 2 bits. - * bitRightShift(field("field1"), 2); - * ``` - * - * @param xValue An expression returning bits. - * @param y The right operand constant representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ -export function bitRightShift(xValue: Expr, y: number): FunctionExpr; -/** - * @beta - * - * Creates an expression that applies a bitwise right shift operation between two expressions. - * - * ```typescript - * // Calculate the bitwise right shift of 'field1' by 'field2' bits. - * bitRightShift(field("field1"), field("field2")); - * ``` - * - * @param xValue An expression returning bits. - * @param right The right operand expression representing the number of bits to shift. - * @return A new {@code Expr} representing the bitwise right shift operation. - */ -export function bitRightShift(xValue: Expr, numberExpr: Expr): FunctionExpr; -export function bitRightShift( - xValue: string | Expr, - numberExpr: number | Expr -): FunctionExpr { - return fieldOrExpression(xValue).bitRightShift( - valueToDefaultExpr(numberExpr) - ); -} - /** * @beta * Creates an expression that indexes into an array from the beginning or end @@ -2951,7 +2680,10 @@ export function bitRightShift( * @param offset The index of the element to return. * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayGet(arrayField: string, offset: number): FunctionExpr; +export function arrayGet( + arrayField: string, + offset: number +): FunctionExpression; /** * @beta @@ -2969,7 +2701,10 @@ export function arrayGet(arrayField: string, offset: number): FunctionExpr; * @param offsetExpr An Expr evaluating to the index of the element to return. * @return A new Expr representing the 'arrayGet' operation. */ -export function arrayGet(arrayField: string, offsetExpr: Expr): FunctionExpr; +export function arrayGet( + arrayField: string, + offsetExpr: Expression +): FunctionExpression; /** * @beta @@ -2987,9 +2722,9 @@ export function arrayGet(arrayField: string, offsetExpr: Expr): FunctionExpr; * @return A new Expr representing the 'arrayGet' operation. */ export function arrayGet( - arrayExpression: Expr, + arrayExpression: Expression, offset: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -3008,13 +2743,13 @@ export function arrayGet( * @return A new Expr representing the 'arrayGet' operation. */ export function arrayGet( - arrayExpression: Expr, - offsetExpr: Expr -): FunctionExpr; + arrayExpression: Expression, + offsetExpr: Expression +): FunctionExpression; export function arrayGet( - array: Expr | string, - offset: Expr | number -): FunctionExpr { + array: Expression | string, + offset: Expression | number +): FunctionExpression { return fieldOrExpression(array).arrayGet(valueToDefaultExpr(offset)); } @@ -3031,7 +2766,7 @@ export function arrayGet( * @param value The expression to check. * @return A new {@code Expr} representing the 'isError' check. */ -export function isError(value: Expr): BooleanExpr { +export function isError(value: Expression): BooleanExpression { return value.isError(); } @@ -3052,7 +2787,10 @@ export function isError(value: Expr): BooleanExpr { * returned if the tryExpr produces an error. * @return A new {@code Expr} representing the 'ifError' operation. */ -export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; +export function ifError( + tryExpr: Expression, + catchExpr: Expression +): FunctionExpression; /** * @beta @@ -3071,8 +2809,14 @@ export function ifError(tryExpr: Expr, catchExpr: Expr): FunctionExpr; * error. * @return A new {@code Expr} representing the 'ifError' operation. */ -export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr; -export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr { +export function ifError( + tryExpr: Expression, + catchValue: unknown +): FunctionExpression; +export function ifError( + tryExpr: Expression, + catchValue: unknown +): FunctionExpression { return tryExpr.ifError(valueToDefaultExpr(catchValue)); } @@ -3090,7 +2834,7 @@ export function ifError(tryExpr: Expr, catchValue: unknown): FunctionExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isAbsent' check. */ -export function isAbsent(value: Expr): BooleanExpr; +export function isAbsent(value: Expression): BooleanExpression; /** * @beta @@ -3106,8 +2850,8 @@ export function isAbsent(value: Expr): BooleanExpr; * @param field The field to check. * @return A new {@code Expr} representing the 'isAbsent' check. */ -export function isAbsent(field: string): BooleanExpr; -export function isAbsent(value: Expr | string): BooleanExpr { +export function isAbsent(field: string): BooleanExpression; +export function isAbsent(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isAbsent(); } @@ -3124,7 +2868,7 @@ export function isAbsent(value: Expr | string): BooleanExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNull(value: Expr): BooleanExpr; +export function isNull(value: Expression): BooleanExpression; /** * @beta @@ -3139,8 +2883,8 @@ export function isNull(value: Expr): BooleanExpr; * @param value The name of the field to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNull(value: string): BooleanExpr; -export function isNull(value: Expr | string): BooleanExpr { +export function isNull(value: string): BooleanExpression; +export function isNull(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isNull(); } @@ -3157,7 +2901,7 @@ export function isNull(value: Expr | string): BooleanExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNotNull(value: Expr): BooleanExpr; +export function isNotNull(value: Expression): BooleanExpression; /** * @beta @@ -3172,8 +2916,8 @@ export function isNotNull(value: Expr): BooleanExpr; * @param value The name of the field to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNotNull(value: string): BooleanExpr; -export function isNotNull(value: Expr | string): BooleanExpr { +export function isNotNull(value: string): BooleanExpression; +export function isNotNull(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isNotNull(); } @@ -3190,7 +2934,7 @@ export function isNotNull(value: Expr | string): BooleanExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isNotNaN' check. */ -export function isNotNan(value: Expr): BooleanExpr; +export function isNotNan(value: Expression): BooleanExpression; /** * @beta @@ -3205,8 +2949,8 @@ export function isNotNan(value: Expr): BooleanExpr; * @param value The name of the field to check. * @return A new {@code Expr} representing the 'isNotNaN' check. */ -export function isNotNan(value: string): BooleanExpr; -export function isNotNan(value: Expr | string): BooleanExpr { +export function isNotNan(value: string): BooleanExpression; +export function isNotNan(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isNotNan(); } @@ -3223,7 +2967,7 @@ export function isNotNan(value: Expr | string): BooleanExpr { * @param mapField The name of a field containing a map value. * @param key The name of the key to remove from the input map. */ -export function mapRemove(mapField: string, key: string): FunctionExpr; +export function mapRemove(mapField: string, key: string): FunctionExpression; /** * @beta * @@ -3237,7 +2981,7 @@ export function mapRemove(mapField: string, key: string): FunctionExpr; * @param mapExpr An expression return a map value. * @param key The name of the key to remove from the input map. */ -export function mapRemove(mapExpr: Expr, key: string): FunctionExpr; +export function mapRemove(mapExpr: Expression, key: string): FunctionExpression; /** * @beta * @@ -3251,7 +2995,10 @@ export function mapRemove(mapExpr: Expr, key: string): FunctionExpr; * @param mapField The name of a field containing a map value. * @param keyExpr An expression that produces the name of the key to remove from the input map. */ -export function mapRemove(mapField: string, keyExpr: Expr): FunctionExpr; +export function mapRemove( + mapField: string, + keyExpr: Expression +): FunctionExpression; /** * @beta * @@ -3265,12 +3012,15 @@ export function mapRemove(mapField: string, keyExpr: Expr): FunctionExpr; * @param mapExpr An expression return a map value. * @param keyExpr An expression that produces the name of the key to remove from the input map. */ -export function mapRemove(mapExpr: Expr, keyExpr: Expr): FunctionExpr; +export function mapRemove( + mapExpr: Expression, + keyExpr: Expression +): FunctionExpression; export function mapRemove( - mapExpr: Expr | string, - stringExpr: Expr | string -): FunctionExpr { + mapExpr: Expression | string, + stringExpr: Expression | string +): FunctionExpression { return fieldOrExpression(mapExpr).mapRemove(valueToDefaultExpr(stringExpr)); } @@ -3282,7 +3032,7 @@ export function mapRemove( * ``` * // 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}, {}) + * mapMerge('settings', { enabled: true }, conditional(field('isAdmin'), { admin: true}, {}) * ``` * * @param mapField Name of a field containing a map value that will be merged. @@ -3293,9 +3043,9 @@ export function mapRemove( */ export function mapMerge( mapField: string, - secondMap: Record | Expr, - ...otherMaps: Array | Expr> -): FunctionExpr; + secondMap: Record | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression; /** * @beta @@ -3305,7 +3055,7 @@ export function mapMerge( * ``` * // 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}, {}) + * mapMerge(field('settings'), { enabled: true }, conditional(field('isAdmin'), { admin: true}, {}) * ``` * * @param firstMap An expression or literal map value that will be merged. @@ -3315,16 +3065,16 @@ export function mapMerge( * as a literal or an expression that returns a map. */ export function mapMerge( - firstMap: Record | Expr, - secondMap: Record | Expr, - ...otherMaps: Array | Expr> -): FunctionExpr; + firstMap: Record | Expression, + secondMap: Record | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression; export function mapMerge( - firstMap: string | Record | Expr, - secondMap: Record | Expr, - ...otherMaps: Array | Expr> -): FunctionExpr { + firstMap: string | Record | Expression, + secondMap: Record | Expression, + ...otherMaps: Array | Expression> +): FunctionExpression { const secondMapExpr = valueToDefaultExpr(secondMap); const otherMapExprs = otherMaps.map(valueToDefaultExpr); return fieldOrExpression(firstMap).mapMerge(secondMapExpr, ...otherMapExprs); @@ -3344,7 +3094,7 @@ export function mapMerge( */ export function documentId( documentPath: string | DocumentReference -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -3358,11 +3108,11 @@ export function documentId( * * @return A new {@code Expr} representing the documentId operation. */ -export function documentId(documentPathExpr: Expr): FunctionExpr; +export function documentId(documentPathExpr: Expression): FunctionExpression; export function documentId( - documentPath: Expr | string | DocumentReference -): FunctionExpr { + documentPath: Expression | string | DocumentReference +): FunctionExpression { // @ts-ignore const documentPathExpr = valueToDefaultExpr(documentPath); return documentPathExpr.documentId(); @@ -3377,11 +3127,11 @@ export function documentId( * @param position Index of the first character of the substring. * @param length Length of the substring. */ -export function substr( +export function substring( field: string, position: number, length?: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -3392,11 +3142,11 @@ export function substr( * @param position Index of the first character of the substring. * @param length Length of the substring. */ -export function substr( - input: Expr, +export function substring( + input: Expression, position: number, length?: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -3407,11 +3157,11 @@ export function substr( * @param position An expression that returns the index of the first character of the substring. * @param length An expression that returns the length of the substring. */ -export function substr( +export function substring( field: string, - position: Expr, - length?: Expr -): FunctionExpr; + position: Expression, + length?: Expression +): FunctionExpression; /** * @beta @@ -3422,22 +3172,22 @@ export function substr( * @param position An expression that returns the index of the first character of the substring. * @param length An expression that returns the length of the substring. */ -export function substr( - input: Expr, - position: Expr, - length?: Expr -): FunctionExpr; - -export function substr( - field: Expr | string, - position: Expr | number, - length?: Expr | number -): FunctionExpr { +export function substring( + input: Expression, + position: Expression, + length?: Expression +): FunctionExpression; + +export function substring( + field: Expression | string, + position: Expression | number, + length?: Expression | number +): FunctionExpression { const fieldExpr = fieldOrExpression(field); const positionExpr = valueToDefaultExpr(position); const lengthExpr = length === undefined ? undefined : valueToDefaultExpr(length); - return fieldExpr.substr(positionExpr, lengthExpr); + return fieldExpr.substring(positionExpr, lengthExpr); } /** @@ -3456,9 +3206,9 @@ export function substr( * @return A new {@code Expr} representing the addition operation. */ export function add( - first: Expr, - second: Expr | unknown, -): FunctionExpr; + first: Expression, + second: Expression | unknown +): FunctionExpression; /** * @beta @@ -3477,16 +3227,14 @@ export function add( */ export function add( fieldName: string, - second: Expr | unknown, -): FunctionExpr; + second: Expression | unknown +): FunctionExpression; export function add( - first: Expr | string, - second: Expr | unknown, -): FunctionExpr { - return fieldOrExpression(first).add( - valueToDefaultExpr(second) - ); + first: Expression | string, + second: Expression | unknown +): FunctionExpression { + return fieldOrExpression(first).add(valueToDefaultExpr(second)); } /** @@ -3503,7 +3251,10 @@ export function add( * @param right The expression to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(left: Expr, right: Expr): FunctionExpr; +export function subtract( + left: Expression, + right: Expression +): FunctionExpression; /** * @beta @@ -3519,7 +3270,10 @@ export function subtract(left: Expr, right: Expr): FunctionExpr; * @param value The constant value to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(expression: Expr, value: unknown): FunctionExpr; +export function subtract( + expression: Expression, + value: unknown +): FunctionExpression; /** * @beta @@ -3535,7 +3289,10 @@ export function subtract(expression: Expr, value: unknown): FunctionExpr; * @param expression The expression to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(fieldName: string, expression: Expr): FunctionExpr; +export function subtract( + fieldName: string, + expression: Expression +): FunctionExpression; /** * @beta @@ -3551,11 +3308,11 @@ export function subtract(fieldName: string, expression: Expr): FunctionExpr; * @param value The constant value to subtract. * @return A new {@code Expr} representing the subtraction operation. */ -export function subtract(fieldName: string, value: unknown): FunctionExpr; +export function subtract(fieldName: string, value: unknown): FunctionExpression; export function subtract( - left: Expr | string, - right: Expr | unknown -): FunctionExpr { + left: Expression | string, + right: Expression | unknown +): FunctionExpression { const normalizedLeft = typeof left === 'string' ? field(left) : left; const normalizedRight = valueToDefaultExpr(right); return normalizedLeft.subtract(normalizedRight); @@ -3577,9 +3334,9 @@ export function subtract( * @return A new {@code Expr} representing the multiplication operation. */ export function multiply( - first: Expr, - second: Expr | unknown, -): FunctionExpr; + first: Expression, + second: Expression | unknown +): FunctionExpression; /** * @beta @@ -3598,16 +3355,14 @@ export function multiply( */ export function multiply( fieldName: string, - second: Expr | unknown -): FunctionExpr; + second: Expression | unknown +): FunctionExpression; export function multiply( - first: Expr | string, - second: Expr | unknown -): FunctionExpr { - return fieldOrExpression(first).multiply( - valueToDefaultExpr(second) - ); + first: Expression | string, + second: Expression | unknown +): FunctionExpression { + return fieldOrExpression(first).multiply(valueToDefaultExpr(second)); } /** @@ -3624,7 +3379,7 @@ export function multiply( * @param right The expression to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(left: Expr, right: Expr): FunctionExpr; +export function divide(left: Expression, right: Expression): FunctionExpression; /** * @beta @@ -3640,7 +3395,10 @@ export function divide(left: Expr, right: Expr): FunctionExpr; * @param value The constant value to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(expression: Expr, value: unknown): FunctionExpr; +export function divide( + expression: Expression, + value: unknown +): FunctionExpression; /** * @beta @@ -3656,7 +3414,10 @@ export function divide(expression: Expr, value: unknown): FunctionExpr; * @param expressions The expression to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(fieldName: string, expressions: Expr): FunctionExpr; +export function divide( + fieldName: string, + expressions: Expression +): FunctionExpression; /** * @beta @@ -3672,11 +3433,11 @@ export function divide(fieldName: string, expressions: Expr): FunctionExpr; * @param value The constant value to divide by. * @return A new {@code Expr} representing the division operation. */ -export function divide(fieldName: string, value: unknown): FunctionExpr; +export function divide(fieldName: string, value: unknown): FunctionExpression; export function divide( - left: Expr | string, - right: Expr | unknown -): FunctionExpr { + left: Expression | string, + right: Expression | unknown +): FunctionExpression { const normalizedLeft = typeof left === 'string' ? field(left) : left; const normalizedRight = valueToDefaultExpr(right); return normalizedLeft.divide(normalizedRight); @@ -3696,7 +3457,7 @@ export function divide( * @param right The divisor expression. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(left: Expr, right: Expr): FunctionExpr; +export function mod(left: Expression, right: Expression): FunctionExpression; /** * @beta @@ -3712,7 +3473,7 @@ export function mod(left: Expr, right: Expr): FunctionExpr; * @param value The divisor constant. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(expression: Expr, value: unknown): FunctionExpr; +export function mod(expression: Expression, value: unknown): FunctionExpression; /** * @beta @@ -3728,7 +3489,10 @@ export function mod(expression: Expr, value: unknown): FunctionExpr; * @param expression The divisor expression. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(fieldName: string, expression: Expr): FunctionExpr; +export function mod( + fieldName: string, + expression: Expression +): FunctionExpression; /** * @beta @@ -3744,8 +3508,11 @@ export function mod(fieldName: string, expression: Expr): FunctionExpr; * @param value The divisor constant. * @return A new {@code Expr} representing the modulo operation. */ -export function mod(fieldName: string, value: unknown): FunctionExpr; -export function mod(left: Expr | string, right: Expr | unknown): FunctionExpr { +export function mod(fieldName: string, value: unknown): FunctionExpression; +export function mod( + left: Expression | string, + right: Expression | unknown +): FunctionExpression { const normalizedLeft = typeof left === 'string' ? field(left) : left; const normalizedRight = valueToDefaultExpr(right); return normalizedLeft.mod(normalizedRight); @@ -3764,11 +3531,14 @@ export function mod(left: Expr | string, right: Expr | unknown): FunctionExpr { * @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 { +export function map(elements: Record): FunctionExpression { return _map(elements, 'map'); } -export function _map(elements: Record, methodName: string | undefined): FunctionExpr { - const result: Expr[] = []; +export function _map( + elements: Record, + methodName: string | undefined +): FunctionExpression { + const result: Expression[] = []; for (const key in elements) { if (Object.prototype.hasOwnProperty.call(elements, key)) { const value = elements[key]; @@ -3776,7 +3546,7 @@ export function _map(elements: Record, methodName: string | und result.push(valueToDefaultExpr(value)); } } - return new FunctionExpr('map', result, 'map'); + return new FunctionExpression('map', result, 'map'); } /** @@ -3791,7 +3561,7 @@ export function _map(elements: Record, methodName: string | und * @param plainObject */ export function _mapValue(plainObject: Record): MapValue { - const result: Map = new Map(); + const result: Map = new Map(); for (const key in plainObject) { if (Object.prototype.hasOwnProperty.call(plainObject, key)) { const value = plainObject[key]; @@ -3814,11 +3584,14 @@ export function _mapValue(plainObject: Record): MapValue { * @param elements The input array to evaluate in the expression. * @return A new {@code Expr} representing the array function. */ -export function array(elements: unknown[]): FunctionExpr { +export function array(elements: unknown[]): FunctionExpression { return _array(elements, 'array'); } -export function _array(elements: unknown[], methodName: string | undefined): FunctionExpr { - return new FunctionExpr( +export function _array( + elements: unknown[], + methodName: string | undefined +): FunctionExpression { + return new FunctionExpression( 'array', elements.map(element => valueToDefaultExpr(element)), methodName @@ -3832,14 +3605,14 @@ export function _array(elements: unknown[], methodName: string | undefined): Fun * * ```typescript * // Check if the 'age' field is equal to an expression - * eq(field("age"), field("minAge").add(10)); + * equal(field("age"), field("minAge").add(10)); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the equality comparison. */ -export function eq(left: Expr, right: Expr): BooleanExpr; +export function equal(left: Expression, right: Expression): BooleanExpression; /** * @beta @@ -3848,14 +3621,17 @@ export function eq(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'age' field is equal to 21 - * eq(field("age"), 21); + * equal(field("age"), 21); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(expression: Expr, value: unknown): BooleanExpr; +export function equal( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -3864,14 +3640,17 @@ export function eq(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the 'age' field is equal to the 'limit' field - * eq("age", field("limit")); + * equal("age", field("limit")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(fieldName: string, expression: Expr): BooleanExpr; +export function equal( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -3880,18 +3659,21 @@ export function eq(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'city' field is equal to string constant "London" - * eq("city", "London"); + * equal("city", "London"); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the equality comparison. */ -export function eq(fieldName: string, value: unknown): BooleanExpr; -export function eq(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function equal(fieldName: string, value: unknown): BooleanExpression; +export function equal( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.eq(rightExpr); + return leftExpr.equal(rightExpr); } /** @@ -3901,14 +3683,17 @@ export function eq(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'status' field is not equal to field 'finalState' - * neq(field("status"), field("finalState")); + * notEqual(field("status"), field("finalState")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the inequality comparison. */ -export function neq(left: Expr, right: Expr): BooleanExpr; +export function notEqual( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -3917,14 +3702,17 @@ export function neq(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'status' field is not equal to "completed" - * neq(field("status"), "completed"); + * notEqual(field("status"), "completed"); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(expression: Expr, value: unknown): BooleanExpr; +export function notEqual( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -3933,14 +3721,17 @@ export function neq(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the 'status' field is not equal to the value of 'expectedStatus' - * neq("status", field("expectedStatus")); + * notEqual("status", field("expectedStatus")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(fieldName: string, expression: Expr): BooleanExpr; +export function notEqual( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -3949,18 +3740,21 @@ export function neq(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'country' field is not equal to "USA" - * neq("country", "USA"); + * notEqual("country", "USA"); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the inequality comparison. */ -export function neq(fieldName: string, value: unknown): BooleanExpr; -export function neq(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function notEqual(fieldName: string, value: unknown): BooleanExpression; +export function notEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.neq(rightExpr); + return leftExpr.notEqual(rightExpr); } /** @@ -3970,14 +3764,17 @@ export function neq(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'age' field is less than 30 - * lt(field("age"), field("limit")); + * lessThan(field("age"), field("limit")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the less than comparison. */ -export function lt(left: Expr, right: Expr): BooleanExpr; +export function lessThan( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -3986,14 +3783,17 @@ export function lt(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'age' field is less than 30 - * lt(field("age"), 30); + * lessThan(field("age"), 30); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(expression: Expr, value: unknown): BooleanExpr; +export function lessThan( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -4002,14 +3802,17 @@ export function lt(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the 'age' field is less than the 'limit' field - * lt("age", field("limit")); + * lessThan("age", field("limit")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(fieldName: string, expression: Expr): BooleanExpr; +export function lessThan( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -4018,18 +3821,21 @@ export function lt(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'price' field is less than 50 - * lt("price", 50); + * lessThan("price", 50); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the less than comparison. */ -export function lt(fieldName: string, value: unknown): BooleanExpr; -export function lt(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function lessThan(fieldName: string, value: unknown): BooleanExpression; +export function lessThan( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.lt(rightExpr); + return leftExpr.lessThan(rightExpr); } /** @@ -4040,14 +3846,17 @@ export function lt(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * lte(field("quantity"), field("limit")); + * lessThan(field("quantity"), field("limit")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(left: Expr, right: Expr): BooleanExpr; +export function lessThanOrEqual( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -4056,28 +3865,34 @@ export function lte(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'quantity' field is less than or equal to 20 - * lte(field("quantity"), 20); + * lessThan(field("quantity"), 20); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(expression: Expr, value: unknown): BooleanExpr; +export function lessThanOrEqual( + expression: Expression, + value: unknown +): BooleanExpression; /** * Creates an expression that checks if a field's value is less than or equal to an expression. * * ```typescript * // Check if the 'quantity' field is less than or equal to the 'limit' field - * lte("quantity", field("limit")); + * lessThan("quantity", field("limit")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(fieldName: string, expression: Expr): BooleanExpr; +export function lessThanOrEqual( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -4086,18 +3901,24 @@ export function lte(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'score' field is less than or equal to 70 - * lte("score", 70); + * lessThan("score", 70); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the less than or equal to comparison. */ -export function lte(fieldName: string, value: unknown): BooleanExpr; -export function lte(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function lessThanOrEqual( + fieldName: string, + value: unknown +): BooleanExpression; +export function lessThanOrEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.lte(rightExpr); + return leftExpr.lessThanOrEqual(rightExpr); } /** @@ -4108,14 +3929,17 @@ export function lte(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'age' field is greater than 18 - * gt(field("age"), Constant(9).add(9)); + * greaterThan(field("age"), Constant(9).add(9)); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the greater than comparison. */ -export function gt(left: Expr, right: Expr): BooleanExpr; +export function greaterThan( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -4124,14 +3948,17 @@ export function gt(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'age' field is greater than 18 - * gt(field("age"), 18); + * greaterThan(field("age"), 18); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(expression: Expr, value: unknown): BooleanExpr; +export function greaterThan( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -4140,14 +3967,17 @@ export function gt(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the value of field 'age' is greater than the value of field 'limit' - * gt("age", field("limit")); + * greaterThan("age", field("limit")); * ``` * * @param fieldName The field name to compare. * @param expression The expression to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(fieldName: string, expression: Expr): BooleanExpr; +export function greaterThan( + fieldName: string, + expression: Expression +): BooleanExpression; /** * @beta @@ -4156,18 +3986,24 @@ export function gt(fieldName: string, expression: Expr): BooleanExpr; * * ```typescript * // Check if the 'price' field is greater than 100 - * gt("price", 100); + * greaterThan("price", 100); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the greater than comparison. */ -export function gt(fieldName: string, value: unknown): BooleanExpr; -export function gt(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function greaterThan( + fieldName: string, + value: unknown +): BooleanExpression; +export function greaterThan( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.gt(rightExpr); + return leftExpr.greaterThan(rightExpr); } /** @@ -4178,14 +4014,17 @@ export function gt(left: Expr | string, right: unknown): BooleanExpr { * * ```typescript * // Check if the 'quantity' field is greater than or equal to the field "threshold" - * gte(field("quantity"), field("threshold")); + * greaterThanOrEqual(field("quantity"), field("threshold")); * ``` * * @param left The first expression to compare. * @param right The second expression to compare. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(left: Expr, right: Expr): BooleanExpr; +export function greaterThanOrEqual( + left: Expression, + right: Expression +): BooleanExpression; /** * @beta @@ -4195,14 +4034,17 @@ export function gte(left: Expr, right: Expr): BooleanExpr; * * ```typescript * // Check if the 'quantity' field is greater than or equal to 10 - * gte(field("quantity"), 10); + * greaterThanOrEqual(field("quantity"), 10); * ``` * * @param expression The expression to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(expression: Expr, value: unknown): BooleanExpr; +export function greaterThanOrEqual( + expression: Expression, + value: unknown +): BooleanExpression; /** * @beta @@ -4211,14 +4053,17 @@ export function gte(expression: Expr, value: unknown): BooleanExpr; * * ```typescript * // Check if the value of field 'age' is greater than or equal to the value of field 'limit' - * gte("age", field("limit")); + * greaterThanOrEqual("age", field("limit")); * ``` * * @param fieldName The field name to compare. * @param value The expression to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(fieldName: string, value: Expr): BooleanExpr; +export function greaterThanOrEqual( + fieldName: string, + value: Expression +): BooleanExpression; /** * @beta @@ -4228,18 +4073,24 @@ export function gte(fieldName: string, value: Expr): BooleanExpr; * * ```typescript * // Check if the 'score' field is greater than or equal to 80 - * gte("score", 80); + * greaterThanOrEqual("score", 80); * ``` * * @param fieldName The field name to compare. * @param value The constant value to compare to. * @return A new `Expr` representing the greater than or equal to comparison. */ -export function gte(fieldName: string, value: unknown): BooleanExpr; -export function gte(left: Expr | string, right: unknown): BooleanExpr { - const leftExpr = left instanceof Expr ? left : field(left); +export function greaterThanOrEqual( + fieldName: string, + value: unknown +): BooleanExpression; +export function greaterThanOrEqual( + left: Expression | string, + right: unknown +): BooleanExpression { + const leftExpr = left instanceof Expression ? left : field(left); const rightExpr = valueToDefaultExpr(right); - return leftExpr.gte(rightExpr); + return leftExpr.greaterThanOrEqual(rightExpr); } /** @@ -4258,10 +4109,10 @@ export function gte(left: Expr | string, right: unknown): BooleanExpr { * @return A new {@code Expr} representing the concatenated array. */ export function arrayConcat( - firstArray: Expr, - secondArray: Expr | unknown[], - ...otherArrays: Array -): FunctionExpr; + firstArray: Expression, + secondArray: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression; /** * @beta @@ -4280,15 +4131,15 @@ export function arrayConcat( */ export function arrayConcat( firstArrayField: string, - secondArray: Expr | unknown[], - ...otherArrays: Array -): FunctionExpr; + secondArray: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression; export function arrayConcat( - firstArray: Expr | string, - secondArray: Expr | unknown[], - ...otherArrays: Array -): FunctionExpr { + firstArray: Expression | string, + secondArray: Expression | unknown[], + ...otherArrays: Array +): FunctionExpression { const exprValues = otherArrays.map(element => valueToDefaultExpr(element)); return fieldOrExpression(firstArray).arrayConcat( fieldOrExpression(secondArray), @@ -4310,7 +4161,10 @@ export function arrayConcat( * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(array: Expr, element: Expr): FunctionExpr; +export function arrayContains( + array: Expression, + element: Expression +): FunctionExpression; /** * @beta @@ -4326,7 +4180,10 @@ export function arrayContains(array: Expr, element: Expr): FunctionExpr; * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(array: Expr, element: unknown): FunctionExpr; +export function arrayContains( + array: Expression, + element: unknown +): FunctionExpression; /** * @beta @@ -4342,7 +4199,10 @@ export function arrayContains(array: Expr, element: unknown): FunctionExpr; * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(fieldName: string, element: Expr): FunctionExpr; +export function arrayContains( + fieldName: string, + element: Expression +): FunctionExpression; /** * @beta @@ -4358,11 +4218,14 @@ export function arrayContains(fieldName: string, element: Expr): FunctionExpr; * @param element The element to search for in the array. * @return A new {@code Expr} representing the 'array_contains' comparison. */ -export function arrayContains(fieldName: string, element: unknown): BooleanExpr; export function arrayContains( - array: Expr | string, + fieldName: string, + element: unknown +): BooleanExpression; +export function arrayContains( + array: Expression | string, element: unknown -): BooleanExpr { +): BooleanExpression { const arrayExpr = fieldOrExpression(array); const elementExpr = valueToDefaultExpr(element); return arrayExpr.arrayContains(elementExpr); @@ -4384,9 +4247,9 @@ export function arrayContains( * @return A new {@code Expr} representing the 'array_contains_any' comparison. */ export function arrayContainsAny( - array: Expr, - values: Array -): BooleanExpr; + array: Expression, + values: Array +): BooleanExpression; /** * @beta @@ -4406,8 +4269,8 @@ export function arrayContainsAny( */ export function arrayContainsAny( fieldName: string, - values: Array -): BooleanExpr; + values: Array +): BooleanExpression; /** * @beta @@ -4424,7 +4287,10 @@ export function arrayContainsAny( * @param values An expression that evaluates to an array, whose elements to check for in the array. * @return A new {@code Expr} representing the 'array_contains_any' comparison. */ -export function arrayContainsAny(array: Expr, values: Expr): BooleanExpr; +export function arrayContainsAny( + array: Expression, + values: Expression +): BooleanExpression; /** * @beta @@ -4442,11 +4308,14 @@ export function arrayContainsAny(array: Expr, values: Expr): BooleanExpr; * @param values An expression that evaluates to an array, whose elements to check for in the array field. * @return A new {@code Expr} representing the 'array_contains_any' comparison. */ -export function arrayContainsAny(fieldName: string, values: Expr): BooleanExpr; export function arrayContainsAny( - array: Expr | string, - values: unknown[] | Expr -): BooleanExpr { + fieldName: string, + values: Expression +): BooleanExpression; +export function arrayContainsAny( + array: Expression | string, + values: unknown[] | Expression +): BooleanExpression { // @ts-ignore implementation accepts both types return fieldOrExpression(array).arrayContainsAny(values); } @@ -4466,9 +4335,9 @@ export function arrayContainsAny( * @return A new {@code Expr} representing the 'array_contains_all' comparison. */ export function arrayContainsAll( - array: Expr, - values: Array -): BooleanExpr; + array: Expression, + values: Array +): BooleanExpression; /** * @beta @@ -4487,8 +4356,8 @@ export function arrayContainsAll( */ export function arrayContainsAll( fieldName: string, - values: Array -): BooleanExpr; + values: Array +): BooleanExpression; /** * @beta @@ -4505,9 +4374,9 @@ export function arrayContainsAll( * @return A new {@code Expr} representing the 'array_contains_all' comparison. */ export function arrayContainsAll( - array: Expr, - arrayExpression: Expr -): BooleanExpr; + array: Expression, + arrayExpression: Expression +): BooleanExpression; /** * @beta @@ -4526,12 +4395,12 @@ export function arrayContainsAll( */ export function arrayContainsAll( fieldName: string, - arrayExpression: Expr -): BooleanExpr; + arrayExpression: Expression +): BooleanExpression; export function arrayContainsAll( - array: Expr | string, - values: unknown[] | Expr -): BooleanExpr { + array: Expression | string, + values: unknown[] | Expression +): BooleanExpression { // @ts-ignore implementation accepts both types return fieldOrExpression(array).arrayContainsAll(values); } @@ -4549,7 +4418,7 @@ export function arrayContainsAll( * @param fieldName The name of the field containing an array to calculate the length of. * @return A new {@code Expr} representing the length of the array. */ -export function arrayLength(fieldName: string): FunctionExpr; +export function arrayLength(fieldName: string): FunctionExpression; /** * @beta @@ -4564,8 +4433,8 @@ export function arrayLength(fieldName: string): FunctionExpr; * @param array The array expression to calculate the length of. * @return A new {@code Expr} representing the length of the array. */ -export function arrayLength(array: Expr): FunctionExpr; -export function arrayLength(array: Expr | string): FunctionExpr { +export function arrayLength(array: Expression): FunctionExpression; +export function arrayLength(array: Expression | string): FunctionExpression { return fieldOrExpression(array).arrayLength(); } @@ -4577,17 +4446,17 @@ export function arrayLength(array: Expr | string): FunctionExpr { * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny(field("category"), [constant("Electronics"), field("primaryType")]); + * equalAny(field("category"), [constant("Electronics"), field("primaryType")]); * ``` * * @param expression The expression whose results to compare. * @param values The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny( - expression: Expr, - values: Array -): BooleanExpr; +export function equalAny( + expression: Expression, + values: Array +): BooleanExpression; /** * @beta @@ -4596,14 +4465,17 @@ export function eqAny( * * ```typescript * // Check if the 'category' field is set to a value in the disabledCategories field - * eqAny(field("category"), field('disabledCategories')); + * equalAny(field("category"), field('disabledCategories')); * ``` * * @param expression The expression whose results to compare. * @param arrayExpression An expression that evaluates to an array, whose elements to check for equality to the input. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr; +export function equalAny( + expression: Expression, + arrayExpression: Expression +): BooleanExpression; /** * @beta @@ -4613,17 +4485,17 @@ export function eqAny(expression: Expr, arrayExpression: Expr): BooleanExpr; * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny("category", [constant("Electronics"), field("primaryType")]); + * equalAny("category", [constant("Electronics"), field("primaryType")]); * ``` * * @param fieldName The field to compare. * @param values The values to check against. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny( +export function equalAny( fieldName: string, - values: Array -): BooleanExpr; + values: Array +): BooleanExpression; /** * @beta @@ -4633,20 +4505,23 @@ export function eqAny( * * ```typescript * // Check if the 'category' field is either "Electronics" or value of field 'primaryType' - * eqAny("category", ["Electronics", field("primaryType")]); + * equalAny("category", ["Electronics", field("primaryType")]); * ``` * * @param fieldName The field to compare. * @param arrayExpression An expression that evaluates to an array, whose elements to check for equality to the input field. * @return A new {@code Expr} representing the 'IN' comparison. */ -export function eqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; -export function eqAny( - element: Expr | string, - values: unknown[] | Expr -): BooleanExpr { +export function equalAny( + fieldName: string, + arrayExpression: Expression +): BooleanExpression; +export function equalAny( + element: Expression | string, + values: unknown[] | Expression +): BooleanExpression { // @ts-ignore implementation accepts both types - return fieldOrExpression(element).eqAny(values); + return fieldOrExpression(element).equalAny(values); } /** @@ -4657,17 +4532,17 @@ export function eqAny( * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notEqAny(field("status"), ["pending", field("rejectedStatus")]); + * notEqualAny(field("status"), ["pending", field("rejectedStatus")]); * ``` * * @param element The expression to compare. * @param values The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny( - element: Expr, - values: Array -): BooleanExpr; +export function notEqualAny( + element: Expression, + values: Array +): BooleanExpression; /** * @beta @@ -4677,17 +4552,17 @@ export function notEqAny( * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of 'rejectedStatus' - * notEqAny("status", [constant("pending"), field("rejectedStatus")]); + * notEqualAny("status", [constant("pending"), field("rejectedStatus")]); * ``` * * @param fieldName The field name to compare. * @param values The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny( +export function notEqualAny( fieldName: string, - values: Array -): BooleanExpr; + values: Array +): BooleanExpression; /** * @beta @@ -4697,14 +4572,17 @@ export function notEqAny( * * ```typescript * // Check if the 'status' field is neither "pending" nor the value of the field 'rejectedStatus' - * notEqAny(field("status"), ["pending", field("rejectedStatus")]); + * notEqualAny(field("status"), ["pending", field("rejectedStatus")]); * ``` * * @param element The expression to compare. * @param arrayExpression The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny(element: Expr, arrayExpression: Expr): BooleanExpr; +export function notEqualAny( + element: Expression, + arrayExpression: Expression +): BooleanExpression; /** * @beta @@ -4713,21 +4591,24 @@ export function notEqAny(element: Expr, arrayExpression: Expr): BooleanExpr; * * ```typescript * // Check if the 'status' field is not equal to any value in the field 'rejectedStatuses' - * notEqAny("status", field("rejectedStatuses")); + * notEqualAny("status", field("rejectedStatuses")); * ``` * * @param fieldName The field name to compare. * @param arrayExpression The values to check against. * @return A new {@code Expr} representing the 'NOT IN' comparison. */ -export function notEqAny(fieldName: string, arrayExpression: Expr): BooleanExpr; +export function notEqualAny( + fieldName: string, + arrayExpression: Expression +): BooleanExpression; -export function notEqAny( - element: Expr | string, - values: unknown[] | Expr -): BooleanExpr { +export function notEqualAny( + element: Expression | string, + values: unknown[] | Expression +): BooleanExpression { // @ts-ignore implementation accepts both types - return fieldOrExpression(element).notEqAny(values); + return fieldOrExpression(element).notEqualAny(values); } /** @@ -4739,9 +4620,9 @@ export function notEqAny( * // Check if only one of the conditions is true: 'age' greater than 18, 'city' is "London", * // or 'status' is "active". * const condition = xor( - * gt("age", 18), - * eq("city", "London"), - * eq("status", "active")); + * greaterThan("age", 18), + * equal("city", "London"), + * equal("status", "active")); * ``` * * @param first The first condition. @@ -4750,11 +4631,15 @@ export function notEqAny( * @return A new {@code Expr} representing the logical 'XOR' operation. */ export function xor( - first: BooleanExpr, - second: BooleanExpr, - ...additionalConditions: BooleanExpr[] -): BooleanExpr { - return new BooleanExpr('xor', [first, second, ...additionalConditions], 'xor'); + first: BooleanExpression, + second: BooleanExpression, + ...additionalConditions: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression( + 'xor', + [first, second, ...additionalConditions], + 'xor' + ); } /** @@ -4765,8 +4650,8 @@ export function xor( * * ```typescript * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". - * cond( - * gt("age", 18), constant("Adult"), constant("Minor")); + * conditional( + * greaterThan("age", 18), constant("Adult"), constant("Minor")); * ``` * * @param condition The condition to evaluate. @@ -4774,12 +4659,16 @@ export function xor( * @param elseExpr The expression to evaluate if the condition is false. * @return A new {@code Expr} representing the conditional expression. */ -export function cond( - condition: BooleanExpr, - thenExpr: Expr, - elseExpr: Expr -): FunctionExpr { - return new FunctionExpr('cond', [condition, thenExpr, elseExpr], 'cond'); +export function conditional( + condition: BooleanExpression, + thenExpr: Expression, + elseExpr: Expression +): FunctionExpression { + return new FunctionExpression( + 'conditional', + [condition, thenExpr, elseExpr], + 'conditional' + ); } /** @@ -4789,13 +4678,13 @@ export function cond( * * ```typescript * // Find documents where the 'completed' field is NOT true - * not(eq("completed", true)); + * not(equal("completed", true)); * ``` * * @param booleanExpr The filter condition to negate. * @return A new {@code Expr} representing the negated filter condition. */ -export function not(booleanExpr: BooleanExpr): BooleanExpr { +export function not(booleanExpr: BooleanExpression): BooleanExpression { return booleanExpr.not(); } @@ -4814,13 +4703,13 @@ export function not(booleanExpr: BooleanExpr): BooleanExpr { * @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. + * @return A new {@code Expr} representing the logical maximum operation. */ export function logicalMaximum( - first: Expr, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; /** * @beta @@ -4837,19 +4726,19 @@ export function logicalMaximum( * @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. + * @return A new {@code Expr} representing the logical maximum operation. */ export function logicalMaximum( fieldName: string, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + second: Expression | unknown, + ...others: Array +): FunctionExpression; export function logicalMaximum( - first: Expr | string, - second: Expr | unknown, - ...others: Array -): FunctionExpr { + first: Expression | string, + second: Expression | unknown, + ...others: Array +): FunctionExpression { return fieldOrExpression(first).logicalMaximum( valueToDefaultExpr(second), ...others.map(value => valueToDefaultExpr(value)) @@ -4871,13 +4760,13 @@ export function logicalMaximum( * @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. + * @return A new {@code Expr} representing the logical minimum operation. */ export function logicalMinimum( - first: Expr, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; /** * @beta @@ -4895,19 +4784,19 @@ export function logicalMinimum( * @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. + * @return A new {@code Expr} representing the logical minimum operation. */ export function logicalMinimum( fieldName: string, - second: Expr | unknown, - ...others: Array -): FunctionExpr; + second: Expression | unknown, + ...others: Array +): FunctionExpression; export function logicalMinimum( - first: Expr | string, - second: Expr | unknown, - ...others: Array -): FunctionExpr { + first: Expression | string, + second: Expression | unknown, + ...others: Array +): FunctionExpression { return fieldOrExpression(first).logicalMinimum( valueToDefaultExpr(second), ...others.map(value => valueToDefaultExpr(value)) @@ -4927,7 +4816,7 @@ export function logicalMinimum( * @param value An expression evaluates to the name of the field to check. * @return A new {@code Expr} representing the 'exists' check. */ -export function exists(value: Expr): BooleanExpr; +export function exists(value: Expression): BooleanExpression; /** * @beta @@ -4942,8 +4831,8 @@ export function exists(value: Expr): BooleanExpr; * @param fieldName The field name to check. * @return A new {@code Expr} representing the 'exists' check. */ -export function exists(fieldName: string): BooleanExpr; -export function exists(valueOrField: Expr | string): BooleanExpr { +export function exists(fieldName: string): BooleanExpression; +export function exists(valueOrField: Expression | string): BooleanExpression { return fieldOrExpression(valueOrField).exists(); } @@ -4960,7 +4849,7 @@ export function exists(valueOrField: Expr | string): BooleanExpr { * @param value The expression to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNan(value: Expr): BooleanExpr; +export function isNan(value: Expression): BooleanExpression; /** * @beta @@ -4975,8 +4864,8 @@ export function isNan(value: Expr): BooleanExpr; * @param fieldName The name of the field to check. * @return A new {@code Expr} representing the 'isNaN' check. */ -export function isNan(fieldName: string): BooleanExpr; -export function isNan(value: Expr | string): BooleanExpr { +export function isNan(fieldName: string): BooleanExpression; +export function isNan(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isNan(); } @@ -4993,7 +4882,7 @@ export function isNan(value: Expr | string): BooleanExpr { * @param stringExpression An expression evaluating to a string value, which will be reversed. * @return A new {@code Expr} representing the reversed string. */ -export function reverse(stringExpression: Expr): FunctionExpr; +export function reverse(stringExpression: Expression): FunctionExpression; /** * @beta @@ -5008,191 +4897,160 @@ export function reverse(stringExpression: Expr): FunctionExpr; * @param field The name of the field representing the string to reverse. * @return A new {@code Expr} representing the reversed string. */ -export function reverse(field: string): FunctionExpr; -export function reverse(expr: Expr | string): FunctionExpr { +export function reverse(field: string): FunctionExpression; +export function reverse(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).reverse(); } /** * @beta * - * Creates an expression that replaces the first occurrence of a substring within a string with another substring. + * Creates an expression that calculates the byte length of a string in UTF-8, or just the length of a Blob. * * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field. - * replaceFirst(field("message"), "hello", "hi"); + * // Calculate the length of the 'myString' field in bytes. + * byteLength(field("myString")); * ``` * - * @param value The expression representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. + * @param expr The expression representing the string. + * @return A new {@code Expr} representing the length of the string in bytes. */ -export function replaceFirst( - value: Expr, - find: string, - replace: string -): FunctionExpr; +export function byteLength(expr: Expression): FunctionExpression; /** * @beta * - * Creates an expression that replaces the first occurrence of a substring within a string with another substring, - * where the substring to find and the replacement substring are specified by expressions. + * Creates an expression that calculates the length of a string represented by a field in UTF-8 bytes, or just the length of a Blob. * * ```typescript - * // Replace the first occurrence of the value in 'findField' with the value in 'replaceField' in the 'message' field. - * replaceFirst(field("message"), field("findField"), field("replaceField")); + * // Calculate the length of the 'myString' field in bytes. + * byteLength("myString"); * ``` * - * @param value The expression representing the string to perform the replacement on. - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace the first occurrence of 'find' with. - * @return A new {@code Expr} representing the string with the first occurrence replaced. + * @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 replaceFirst( - value: Expr, - find: Expr, - replace: Expr -): FunctionExpr; +export function byteLength(fieldName: string): FunctionExpression; +export function byteLength(expr: Expression | string): FunctionExpression { + const normalizedExpr = fieldOrExpression(expr); + return normalizedExpr.byteLength(); +} /** - * @beta - * - * Creates an expression that replaces the first occurrence of a substring within a string represented by a field with another substring. + * Creates an expression that reverses an array. * * ```typescript - * // Replace the first occurrence of "hello" with "hi" in the 'message' field. - * replaceFirst("message", "hello", "hi"); + * // Reverse the value of the 'myArray' field. + * arrayReverse("myArray"); * ``` * - * @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. + * @param fieldName The name of the field to reverse. + * @return A new {@code Expr} representing the reversed array. */ -export function replaceFirst( - fieldName: string, - find: string, - replace: string -): FunctionExpr; -export function replaceFirst( - value: Expr | string, - find: Expr | string, - replace: Expr | string -): FunctionExpr { - const normalizedValue = fieldOrExpression(value); - const normalizedFind = valueToDefaultExpr(find); - const normalizedReplace = valueToDefaultExpr(replace); - return normalizedValue.replaceFirst(normalizedFind, normalizedReplace); -} +export function arrayReverse(fieldName: string): FunctionExpression; /** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string with another substring. + * Creates an expression that reverses an array. * * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field. - * replaceAll(field("message"), "hello", "hi"); + * // Reverse the value of the 'myArray' field. + * arrayReverse(field("myArray")); * ``` * - * @param value The expression representing the string to perform the replacement on. - * @param find The substring to search for. - * @param replace The substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. + * @param arrayExpression An expression evaluating to an array value, which will be reversed. + * @return A new {@code Expr} representing the reversed array. */ -export function replaceAll( - value: Expr, - find: string, - replace: string -): FunctionExpr; +export function arrayReverse(arrayExpression: Expression): FunctionExpression; +export function arrayReverse(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).arrayReverse(); +} /** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string with another substring, - * where the substring to find and the replacement substring are specified by expressions. + * Creates an expression that computes e to the power of the expression's result. * * ```typescript - * // Replace all occurrences of the value in 'findField' with the value in 'replaceField' in the 'message' field. - * replaceAll(field("message"), field("findField"), field("replaceField")); + * // Compute e to the power of 2. + * exp(constant(2)); * ``` * - * @param value The expression representing the string to perform the replacement on. - * @param find The expression representing the substring to search for. - * @param replace The expression representing the substring to replace all occurrences of 'find' with. - * @return A new {@code Expr} representing the string with all occurrences replaced. + * @return A new {@code Expr} representing the exp of the numeric value. */ -export function replaceAll( - value: Expr, - find: Expr, - replace: Expr -): FunctionExpr; +export function exp(expression: Expression): FunctionExpression; /** - * @beta - * - * Creates an expression that replaces all occurrences of a substring within a string represented by a field with another substring. + * Creates an expression that computes e to the power of the expression's result. * * ```typescript - * // Replace all occurrences of "hello" with "hi" in the 'message' field. - * replaceAll("message", "hello", "hi"); + * // Compute e to the power of the 'value' field. + * exp('value'); * ``` * - * @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. + * @return A new {@code Expr} representing the exp of the numeric value. */ -export function replaceAll( - fieldName: string, - find: string, - replace: string -): FunctionExpr; -export function replaceAll( - value: Expr | string, - find: Expr | string, - replace: Expr | string -): FunctionExpr { - const normalizedValue = fieldOrExpression(value); - const normalizedFind = valueToDefaultExpr(find); - const normalizedReplace = valueToDefaultExpr(replace); - return normalizedValue.replaceAll(normalizedFind, normalizedReplace); +export function exp(fieldName: string): FunctionExpression; + +export function exp( + expressionOrFieldName: Expression | string +): FunctionExpression { + return fieldOrExpression(expressionOrFieldName).exp(); } /** - * @beta - * - * Creates an expression that calculates the byte length of a string in UTF-8, or just the length of a Blob. + * Creates an expression that computes the ceiling of a numeric value. * * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * byteLength(field("myString")); + * // Compute the ceiling of the 'price' field. + * ceil("price"); * ``` * - * @param expr The expression representing the string. - * @return A new {@code Expr} representing the length of the string in bytes. + * @param fieldName The name of the field to compute the ceiling of. + * @return A new {@code Expr} representing the ceiling of the numeric value. */ -export function byteLength(expr: Expr): FunctionExpr; +export function ceil(fieldName: string): FunctionExpression; /** - * @beta - * - * Creates an expression that calculates the length of a string represented by a field in UTF-8 bytes, or just the length of a Blob. + * Creates an expression that computes the ceiling of a numeric value. * * ```typescript - * // Calculate the length of the 'myString' field in bytes. - * byteLength("myString"); + * // Compute the ceiling of the 'price' field. + * ceil(field("price")); * ``` * - * @param fieldName The name of the field containing the string. - * @return A new {@code Expr} representing the length of the string in bytes. + * @param expression An expression evaluating to a numeric value, which the ceiling will be computed for. + * @return A new {@code Expr} representing the ceiling of the numeric value. */ -export function byteLength(fieldName: string): FunctionExpr; -export function byteLength(expr: Expr | string): FunctionExpr { - const normalizedExpr = fieldOrExpression(expr); - return normalizedExpr.byteLength(); +export function ceil(expression: Expression): FunctionExpression; +export function ceil(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).ceil(); +} + +/** + * Creates an expression that computes the floor of a numeric value. + * + * @param expr The expression to compute the floor of. + * @return A new {@code Expr} representing the floor of the numeric value. + */ +export function floor(expr: Expression): FunctionExpression; + +/** + * Creates an expression that computes the floor of a numeric value. + * + * @param fieldName The name of the field to compute the floor of. + * @return A new {@code Expr} representing the floor of the numeric value. + */ +export function floor(fieldName: string): FunctionExpression; +export function floor(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).floor(); +} + +/** + * Creates an aggregation that counts the number of distinct values of a field. + * + * @param expr The expression or field to count distinct values of. + * @return A new `AggregateFunction` representing the 'count_distinct' aggregation. + */ +export function countDistinct(expr: Expression | string): AggregateFunction { + return fieldOrExpression(expr).countDistinct(); } /** @@ -5208,7 +5066,7 @@ export function byteLength(expr: Expr | string): FunctionExpr { * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the length of the string. */ -export function charLength(fieldName: string): FunctionExpr; +export function charLength(fieldName: string): FunctionExpression; /** * @beta @@ -5223,8 +5081,8 @@ export function charLength(fieldName: string): FunctionExpr; * @param stringExpression The expression representing the string to calculate the length of. * @return A new {@code Expr} representing the length of the string. */ -export function charLength(stringExpression: Expr): FunctionExpr; -export function charLength(value: Expr | string): FunctionExpr { +export function charLength(stringExpression: Expression): FunctionExpression; +export function charLength(value: Expression | string): FunctionExpression { const valueExpr = fieldOrExpression(value); return valueExpr.charLength(); } @@ -5244,7 +5102,7 @@ export function charLength(value: Expr | string): FunctionExpr { * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new {@code Expr} representing the 'like' comparison. */ -export function like(fieldName: string, pattern: string): BooleanExpr; +export function like(fieldName: string, pattern: string): BooleanExpression; /** * @beta @@ -5261,7 +5119,7 @@ export function like(fieldName: string, pattern: string): BooleanExpr; * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new {@code Expr} representing the 'like' comparison. */ -export function like(fieldName: string, pattern: Expr): BooleanExpr; +export function like(fieldName: string, pattern: Expression): BooleanExpression; /** * @beta @@ -5277,7 +5135,10 @@ export function like(fieldName: string, pattern: Expr): BooleanExpr; * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new {@code Expr} representing the 'like' comparison. */ -export function like(stringExpression: Expr, pattern: string): BooleanExpr; +export function like( + stringExpression: Expression, + pattern: string +): BooleanExpression; /** * @beta @@ -5293,11 +5154,14 @@ export function like(stringExpression: Expr, pattern: string): BooleanExpr; * @param pattern The pattern to search for. You can use "%" as a wildcard character. * @return A new {@code Expr} representing the 'like' comparison. */ -export function like(stringExpression: Expr, pattern: Expr): BooleanExpr; export function like( - left: Expr | string, - pattern: Expr | string -): BooleanExpr { + stringExpression: Expression, + pattern: Expression +): BooleanExpression; +export function like( + left: Expression | string, + pattern: Expression | string +): BooleanExpression { const leftExpr = fieldOrExpression(left); const patternExpr = valueToDefaultExpr(pattern); return leftExpr.like(patternExpr); @@ -5318,7 +5182,10 @@ export function like( * @param pattern The regular expression to use for the search. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function regexContains(fieldName: string, pattern: string): BooleanExpr; +export function regexContains( + fieldName: string, + pattern: string +): BooleanExpression; /** * @beta @@ -5335,7 +5202,10 @@ export function regexContains(fieldName: string, pattern: string): BooleanExpr; * @param pattern The regular expression to use for the search. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function regexContains(fieldName: string, pattern: Expr): BooleanExpr; +export function regexContains( + fieldName: string, + pattern: Expression +): BooleanExpression; /** * @beta @@ -5353,9 +5223,9 @@ export function regexContains(fieldName: string, pattern: Expr): BooleanExpr; * @return A new {@code Expr} representing the 'contains' comparison. */ export function regexContains( - stringExpression: Expr, + stringExpression: Expression, pattern: string -): BooleanExpr; +): BooleanExpression; /** * @beta @@ -5373,13 +5243,13 @@ export function regexContains( * @return A new {@code Expr} representing the 'contains' comparison. */ export function regexContains( - stringExpression: Expr, - pattern: Expr -): BooleanExpr; + stringExpression: Expression, + pattern: Expression +): BooleanExpression; export function regexContains( - left: Expr | string, - pattern: Expr | string -): BooleanExpr { + left: Expression | string, + pattern: Expression | string +): BooleanExpression { const leftExpr = fieldOrExpression(left); const patternExpr = valueToDefaultExpr(pattern); return leftExpr.regexContains(patternExpr); @@ -5399,7 +5269,10 @@ export function regexContains( * @param pattern The regular expression to use for the match. * @return A new {@code Expr} representing the regular expression match. */ -export function regexMatch(fieldName: string, pattern: string): BooleanExpr; +export function regexMatch( + fieldName: string, + pattern: string +): BooleanExpression; /** * @beta @@ -5415,7 +5288,10 @@ export function regexMatch(fieldName: string, pattern: string): BooleanExpr; * @param pattern The regular expression to use for the match. * @return A new {@code Expr} representing the regular expression match. */ -export function regexMatch(fieldName: string, pattern: Expr): BooleanExpr; +export function regexMatch( + fieldName: string, + pattern: Expression +): BooleanExpression; /** * @beta @@ -5433,9 +5309,9 @@ export function regexMatch(fieldName: string, pattern: Expr): BooleanExpr; * @return A new {@code Expr} representing the regular expression match. */ export function regexMatch( - stringExpression: Expr, + stringExpression: Expression, pattern: string -): BooleanExpr; +): BooleanExpression; /** * @beta @@ -5452,11 +5328,14 @@ export function regexMatch( * @param pattern The regular expression to use for the match. * @return A new {@code Expr} representing the regular expression match. */ -export function regexMatch(stringExpression: Expr, pattern: Expr): BooleanExpr; export function regexMatch( - left: Expr | string, - pattern: Expr | string -): BooleanExpr { + stringExpression: Expression, + pattern: Expression +): BooleanExpression; +export function regexMatch( + left: Expression | string, + pattern: Expression | string +): BooleanExpression { const leftExpr = fieldOrExpression(left); const patternExpr = valueToDefaultExpr(pattern); return leftExpr.regexMatch(patternExpr); @@ -5469,14 +5348,17 @@ export function regexMatch( * * ```typescript * // Check if the 'description' field contains "example". - * strContains("description", "example"); + * stringContains("description", "example"); * ``` * * @param fieldName The name of the field containing the string. * @param substring The substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains(fieldName: string, substring: string): BooleanExpr; +export function stringContains( + fieldName: string, + substring: string +): BooleanExpression; /** * @beta @@ -5485,14 +5367,17 @@ export function strContains(fieldName: string, substring: string): BooleanExpr; * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * strContains("description", field("keyword")); + * stringContains("description", field("keyword")); * ``` * * @param fieldName The name of the field containing the string. * @param substring The expression representing the substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains(fieldName: string, substring: Expr): BooleanExpr; +export function stringContains( + fieldName: string, + substring: Expression +): BooleanExpression; /** * @beta @@ -5501,17 +5386,17 @@ export function strContains(fieldName: string, substring: Expr): BooleanExpr; * * ```typescript * // Check if the 'description' field contains "example". - * strContains(field("description"), "example"); + * stringContains(field("description"), "example"); * ``` * * @param stringExpression The expression representing the string to perform the comparison on. * @param substring The substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains( - stringExpression: Expr, +export function stringContains( + stringExpression: Expression, substring: string -): BooleanExpr; +): BooleanExpression; /** * @beta @@ -5520,24 +5405,24 @@ export function strContains( * * ```typescript * // Check if the 'description' field contains the value of the 'keyword' field. - * strContains(field("description"), field("keyword")); + * stringContains(field("description"), field("keyword")); * ``` * * @param stringExpression The expression representing the string to perform the comparison on. * @param substring The expression representing the substring to search for. * @return A new {@code Expr} representing the 'contains' comparison. */ -export function strContains( - stringExpression: Expr, - substring: Expr -): BooleanExpr; -export function strContains( - left: Expr | string, - substring: Expr | string -): BooleanExpr { +export function stringContains( + stringExpression: Expression, + substring: Expression +): BooleanExpression; +export function stringContains( + left: Expression | string, + substring: Expression | string +): BooleanExpression { const leftExpr = fieldOrExpression(left); const substringExpr = valueToDefaultExpr(substring); - return leftExpr.strContains(substringExpr); + return leftExpr.stringContains(substringExpr); } /** @@ -5554,7 +5439,10 @@ export function strContains( * @param prefix The prefix to check for. * @return A new {@code Expr} representing the 'starts with' comparison. */ -export function startsWith(fieldName: string, prefix: string): BooleanExpr; +export function startsWith( + fieldName: string, + prefix: string +): BooleanExpression; /** * @beta @@ -5570,7 +5458,10 @@ export function startsWith(fieldName: string, prefix: string): BooleanExpr; * @param prefix The expression representing the prefix. * @return A new {@code Expr} representing the 'starts with' comparison. */ -export function startsWith(fieldName: string, prefix: Expr): BooleanExpr; +export function startsWith( + fieldName: string, + prefix: Expression +): BooleanExpression; /** * @beta @@ -5586,7 +5477,10 @@ export function startsWith(fieldName: string, prefix: Expr): BooleanExpr; * @param prefix The prefix to check for. * @return A new {@code Expr} representing the 'starts with' comparison. */ -export function startsWith(stringExpression: Expr, prefix: string): BooleanExpr; +export function startsWith( + stringExpression: Expression, + prefix: string +): BooleanExpression; /** * @beta @@ -5602,11 +5496,14 @@ export function startsWith(stringExpression: Expr, prefix: string): BooleanExpr; * @param prefix The prefix to check for. * @return A new {@code Expr} representing the 'starts with' comparison. */ -export function startsWith(stringExpression: Expr, prefix: Expr): BooleanExpr; export function startsWith( - expr: Expr | string, - prefix: Expr | string -): BooleanExpr { + stringExpression: Expression, + prefix: Expression +): BooleanExpression; +export function startsWith( + expr: Expression | string, + prefix: Expression | string +): BooleanExpression { return fieldOrExpression(expr).startsWith(valueToDefaultExpr(prefix)); } @@ -5624,7 +5521,7 @@ export function startsWith( * @param suffix The postfix to check for. * @return A new {@code Expr} representing the 'ends with' comparison. */ -export function endsWith(fieldName: string, suffix: string): BooleanExpr; +export function endsWith(fieldName: string, suffix: string): BooleanExpression; /** * @beta @@ -5640,7 +5537,10 @@ export function endsWith(fieldName: string, suffix: string): BooleanExpr; * @param suffix The expression representing the postfix. * @return A new {@code Expr} representing the 'ends with' comparison. */ -export function endsWith(fieldName: string, suffix: Expr): BooleanExpr; +export function endsWith( + fieldName: string, + suffix: Expression +): BooleanExpression; /** * @beta @@ -5656,7 +5556,10 @@ export function endsWith(fieldName: string, suffix: Expr): BooleanExpr; * @param suffix The postfix to check for. * @return A new {@code Expr} representing the 'ends with' comparison. */ -export function endsWith(stringExpression: Expr, suffix: string): BooleanExpr; +export function endsWith( + stringExpression: Expression, + suffix: string +): BooleanExpression; /** * @beta @@ -5672,11 +5575,14 @@ export function endsWith(stringExpression: Expr, suffix: string): BooleanExpr; * @param suffix The postfix to check for. * @return A new {@code Expr} representing the 'ends with' comparison. */ -export function endsWith(stringExpression: Expr, suffix: Expr): BooleanExpr; export function endsWith( - expr: Expr | string, - suffix: Expr | string -): BooleanExpr { + stringExpression: Expression, + suffix: Expression +): BooleanExpression; +export function endsWith( + expr: Expression | string, + suffix: Expression | string +): BooleanExpression { return fieldOrExpression(expr).endsWith(valueToDefaultExpr(suffix)); } @@ -5693,7 +5599,7 @@ export function endsWith( * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the lowercase string. */ -export function toLower(fieldName: string): FunctionExpr; +export function toLower(fieldName: string): FunctionExpression; /** * @beta @@ -5708,8 +5614,8 @@ export function toLower(fieldName: string): FunctionExpr; * @param stringExpression The expression representing the string to convert to lowercase. * @return A new {@code Expr} representing the lowercase string. */ -export function toLower(stringExpression: Expr): FunctionExpr; -export function toLower(expr: Expr | string): FunctionExpr { +export function toLower(stringExpression: Expression): FunctionExpression; +export function toLower(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).toLower(); } @@ -5726,7 +5632,7 @@ export function toLower(expr: Expr | string): FunctionExpr { * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the uppercase string. */ -export function toUpper(fieldName: string): FunctionExpr; +export function toUpper(fieldName: string): FunctionExpression; /** * @beta @@ -5741,8 +5647,8 @@ export function toUpper(fieldName: string): FunctionExpr; * @param stringExpression The expression representing the string to convert to uppercase. * @return A new {@code Expr} representing the uppercase string. */ -export function toUpper(stringExpression: Expr): FunctionExpr; -export function toUpper(expr: Expr | string): FunctionExpr { +export function toUpper(stringExpression: Expression): FunctionExpression; +export function toUpper(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).toUpper(); } @@ -5759,7 +5665,7 @@ export function toUpper(expr: Expr | string): FunctionExpr { * @param fieldName The name of the field containing the string. * @return A new {@code Expr} representing the trimmed string. */ -export function trim(fieldName: string): FunctionExpr; +export function trim(fieldName: string): FunctionExpression; /** * @beta @@ -5774,8 +5680,8 @@ export function trim(fieldName: string): FunctionExpr; * @param stringExpression The expression representing the string to trim. * @return A new {@code Expr} representing the trimmed string. */ -export function trim(stringExpression: Expr): FunctionExpr; -export function trim(expr: Expr | string): FunctionExpr { +export function trim(stringExpression: Expression): FunctionExpression; +export function trim(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).trim(); } @@ -5786,7 +5692,7 @@ export function trim(expr: Expr | string): FunctionExpr { * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * strConcat("firstName", " ", field("lastName")); + * stringConcat("firstName", " ", field("lastName")); * ``` * * @param fieldName The field name containing the initial string value. @@ -5794,11 +5700,11 @@ export function trim(expr: Expr | string): FunctionExpr { * @param otherStrings Optional additional expressions or literals (typically strings) to concatenate. * @return A new {@code Expr} representing the concatenated string. */ -export function strConcat( +export function stringConcat( fieldName: string, - secondString: Expr | string, - ...otherStrings: Array -): FunctionExpr; + secondString: Expression | string, + ...otherStrings: Array +): FunctionExpression; /** * @beta @@ -5806,7 +5712,7 @@ export function strConcat( * * ```typescript * // Combine the 'firstName', " ", and 'lastName' fields into a single string - * strConcat(field("firstName"), " ", field("lastName")); + * stringConcat(field("firstName"), " ", field("lastName")); * ``` * * @param firstString The initial string expression to concatenate to. @@ -5814,17 +5720,17 @@ export function strConcat( * @param otherStrings Optional additional expressions or literals (typically strings) to concatenate. * @return A new {@code Expr} representing the concatenated string. */ -export function strConcat( - firstString: Expr, - secondString: Expr | string, - ...otherStrings: Array -): FunctionExpr; -export function strConcat( - first: string | Expr, - second: string | Expr, - ...elements: Array -): FunctionExpr { - return fieldOrExpression(first).strConcat( +export function stringConcat( + firstString: Expression, + secondString: Expression | string, + ...otherStrings: Array +): FunctionExpression; +export function stringConcat( + first: string | Expression, + second: string | Expression, + ...elements: Array +): FunctionExpression { + return fieldOrExpression(first).stringConcat( valueToDefaultExpr(second), ...elements.map(valueToDefaultExpr) ); @@ -5844,7 +5750,7 @@ export function strConcat( * @param subField The key to access in the map. * @return A new {@code Expr} representing the value associated with the given key in the map. */ -export function mapGet(fieldName: string, subField: string): FunctionExpr; +export function mapGet(fieldName: string, subField: string): FunctionExpression; /** * @beta @@ -5860,11 +5766,14 @@ export function mapGet(fieldName: string, subField: string): FunctionExpr; * @param subField The key to access in the map. * @return A new {@code Expr} representing the value associated with the given key in the map. */ -export function mapGet(mapExpression: Expr, subField: string): FunctionExpr; export function mapGet( - fieldOrExpr: string | Expr, + mapExpression: Expression, + subField: string +): FunctionExpression; +export function mapGet( + fieldOrExpr: string | Expression, subField: string -): FunctionExpr { +): FunctionExpression { return fieldOrExpression(fieldOrExpr).mapGet(subField); } @@ -5892,13 +5801,13 @@ export function countAll(): AggregateFunction { * * ```typescript * // Count the number of items where the price is greater than 10 - * count(field("price").gt(10)).as("expensiveItemCount"); + * count(field("price").greaterThan(10)).as("expensiveItemCount"); * ``` * * @param expression The expression to count. * @return A new {@code AggregateFunction} representing the 'count' aggregation. */ -export function count(expression: Expr): AggregateFunction; +export function count(expression: Expression): AggregateFunction; /** * Creates an aggregation that counts the number of stage inputs where the input field exists. @@ -5912,7 +5821,7 @@ export function count(expression: Expr): AggregateFunction; * @return A new {@code AggregateFunction} representing the 'count' aggregation. */ export function count(fieldName: string): AggregateFunction; -export function count(value: Expr | string): AggregateFunction { +export function count(value: Expression | string): AggregateFunction { return fieldOrExpression(value).count(); } @@ -5930,7 +5839,7 @@ export function count(value: Expr | string): AggregateFunction { * @param expression The expression to sum up. * @return A new {@code AggregateFunction} representing the 'sum' aggregation. */ -export function sum(expression: Expr): AggregateFunction; +export function sum(expression: Expression): AggregateFunction; /** * @beta @@ -5947,7 +5856,7 @@ export function sum(expression: Expr): AggregateFunction; * @return A new {@code AggregateFunction} representing the 'sum' aggregation. */ export function sum(fieldName: string): AggregateFunction; -export function sum(value: Expr | string): AggregateFunction { +export function sum(value: Expression | string): AggregateFunction { return fieldOrExpression(value).sum(); } @@ -5959,13 +5868,13 @@ export function sum(value: Expr | string): AggregateFunction { * * ```typescript * // Calculate the average age of users - * avg(field("age")).as("averageAge"); + * average(field("age")).as("averageAge"); * ``` * * @param expression The expression representing the values to average. - * @return A new {@code AggregateFunction} representing the 'avg' aggregation. + * @return A new {@code AggregateFunction} representing the 'average' aggregation. */ -export function avg(expression: Expr): AggregateFunction; +export function average(expression: Expression): AggregateFunction; /** * @beta @@ -5975,15 +5884,15 @@ export function avg(expression: Expr): AggregateFunction; * * ```typescript * // Calculate the average age of users - * avg("age").as("averageAge"); + * average("age").as("averageAge"); * ``` * * @param fieldName The name of the field containing numeric values to average. - * @return A new {@code AggregateFunction} representing the 'avg' aggregation. + * @return A new {@code AggregateFunction} representing the 'average' aggregation. */ -export function avg(fieldName: string): AggregateFunction; -export function avg(value: Expr | string): AggregateFunction { - return fieldOrExpression(value).avg(); +export function average(fieldName: string): AggregateFunction; +export function average(value: Expression | string): AggregateFunction { + return fieldOrExpression(value).average(); } /** @@ -5998,9 +5907,9 @@ export function avg(value: Expr | string): AggregateFunction { * ``` * * @param expression The expression to find the minimum value of. - * @return A new {@code AggregateFunction} representing the 'min' aggregation. + * @return A new {@code AggregateFunction} representing the 'minimum' aggregation. */ -export function minimum(expression: Expr): AggregateFunction; +export function minimum(expression: Expression): AggregateFunction; /** * @beta @@ -6013,10 +5922,10 @@ export function minimum(expression: Expr): AggregateFunction; * ``` * * @param fieldName The name of the field to find the minimum value of. - * @return A new {@code AggregateFunction} representing the 'min' aggregation. + * @return A new {@code AggregateFunction} representing the 'minimum' aggregation. */ export function minimum(fieldName: string): AggregateFunction; -export function minimum(value: Expr | string): AggregateFunction { +export function minimum(value: Expression | string): AggregateFunction { return fieldOrExpression(value).minimum(); } @@ -6032,9 +5941,9 @@ export function minimum(value: Expr | string): AggregateFunction { * ``` * * @param expression The expression to find the maximum value of. - * @return A new {@code AggregateFunction} representing the 'max' aggregation. + * @return A new {@code AggregateFunction} representing the 'maximum' aggregation. */ -export function maximum(expression: Expr): AggregateFunction; +export function maximum(expression: Expression): AggregateFunction; /** * @beta @@ -6047,10 +5956,10 @@ export function maximum(expression: Expr): AggregateFunction; * ``` * * @param fieldName The name of the field to find the maximum value of. - * @return A new {@code AggregateFunction} representing the 'max' aggregation. + * @return A new {@code AggregateFunction} representing the 'maximum' aggregation. */ export function maximum(fieldName: string): AggregateFunction; -export function maximum(value: Expr | string): AggregateFunction { +export function maximum(value: Expression | string): AggregateFunction { return fieldOrExpression(value).maximum(); } @@ -6071,7 +5980,7 @@ export function maximum(value: Expr | string): AggregateFunction { export function cosineDistance( fieldName: string, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6089,8 +5998,8 @@ export function cosineDistance( */ export function cosineDistance( fieldName: string, - vectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression +): FunctionExpression; /** * @beta @@ -6107,9 +6016,9 @@ export function cosineDistance( * @return A new {@code Expr} representing the cosine distance between the two vectors. */ export function cosineDistance( - vectorExpression: Expr, - vector: number[] | Expr -): FunctionExpr; + vectorExpression: Expression, + vector: number[] | VectorValue +): FunctionExpression; /** * @beta @@ -6126,13 +6035,13 @@ export function cosineDistance( * @return A new {@code Expr} representing the cosine distance between the two vectors. */ export function cosineDistance( - vectorExpression: Expr, - otherVectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression, + otherVectorExpression: Expression +): FunctionExpression; export function cosineDistance( - expr: Expr | string, - other: Expr | number[] | VectorValue -): FunctionExpr { + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { const expr1 = fieldOrExpression(expr); const expr2 = vectorToExpr(other); return expr1.cosineDistance(expr2); @@ -6155,7 +6064,7 @@ export function cosineDistance( export function dotProduct( fieldName: string, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6173,8 +6082,8 @@ export function dotProduct( */ export function dotProduct( fieldName: string, - vectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression +): FunctionExpression; /** * @beta @@ -6191,9 +6100,9 @@ export function dotProduct( * @return A new {@code Expr} representing the dot product between the two vectors. */ export function dotProduct( - vectorExpression: Expr, + vectorExpression: Expression, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6210,13 +6119,13 @@ export function dotProduct( * @return A new {@code Expr} representing the dot product between the two vectors. */ export function dotProduct( - vectorExpression: Expr, - otherVectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression, + otherVectorExpression: Expression +): FunctionExpression; export function dotProduct( - expr: Expr | string, - other: Expr | number[] | VectorValue -): FunctionExpr { + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { const expr1 = fieldOrExpression(expr); const expr2 = vectorToExpr(other); return expr1.dotProduct(expr2); @@ -6239,7 +6148,7 @@ export function dotProduct( export function euclideanDistance( fieldName: string, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6257,8 +6166,8 @@ export function euclideanDistance( */ export function euclideanDistance( fieldName: string, - vectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression +): FunctionExpression; /** * @beta @@ -6276,9 +6185,9 @@ export function euclideanDistance( * @return A new {@code Expr} representing the Euclidean distance between the two vectors. */ export function euclideanDistance( - vectorExpression: Expr, + vectorExpression: Expression, vector: number[] | VectorValue -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6295,13 +6204,13 @@ export function euclideanDistance( * @return A new {@code Expr} representing the Euclidean distance between the two vectors. */ export function euclideanDistance( - vectorExpression: Expr, - otherVectorExpression: Expr -): FunctionExpr; + vectorExpression: Expression, + otherVectorExpression: Expression +): FunctionExpression; export function euclideanDistance( - expr: Expr | string, - other: Expr | number[] | VectorValue -): FunctionExpr { + expr: Expression | string, + other: Expression | number[] | VectorValue +): FunctionExpression { const expr1 = fieldOrExpression(expr); const expr2 = vectorToExpr(other); return expr1.euclideanDistance(expr2); @@ -6320,7 +6229,7 @@ export function euclideanDistance( * @param vectorExpression The expression representing the Firestore Vector. * @return A new {@code Expr} representing the length of the array. */ -export function vectorLength(vectorExpression: Expr): FunctionExpr; +export function vectorLength(vectorExpression: Expression): FunctionExpression; /** * @beta @@ -6335,8 +6244,8 @@ export function vectorLength(vectorExpression: Expr): FunctionExpr; * @param fieldName The name of the field representing the Firestore Vector. * @return A new {@code Expr} representing the length of the array. */ -export function vectorLength(fieldName: string): FunctionExpr; -export function vectorLength(expr: Expr | string): FunctionExpr { +export function vectorLength(fieldName: string): FunctionExpression; +export function vectorLength(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).vectorLength(); } @@ -6354,7 +6263,7 @@ export function vectorLength(expr: Expr | string): FunctionExpr { * @param expr The expression representing the number of microseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMicrosToTimestamp(expr: Expr): FunctionExpr; +export function unixMicrosToTimestamp(expr: Expression): FunctionExpression; /** * @beta @@ -6370,8 +6279,10 @@ export function unixMicrosToTimestamp(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the number of microseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMicrosToTimestamp(fieldName: string): FunctionExpr; -export function unixMicrosToTimestamp(expr: Expr | string): FunctionExpr { +export function unixMicrosToTimestamp(fieldName: string): FunctionExpression; +export function unixMicrosToTimestamp( + expr: Expression | string +): FunctionExpression { return fieldOrExpression(expr).unixMicrosToTimestamp(); } @@ -6388,7 +6299,7 @@ export function unixMicrosToTimestamp(expr: Expr | string): FunctionExpr { * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of microseconds since epoch. */ -export function timestampToUnixMicros(expr: Expr): FunctionExpr; +export function timestampToUnixMicros(expr: Expression): FunctionExpression; /** * @beta @@ -6403,8 +6314,10 @@ export function timestampToUnixMicros(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the timestamp. * @return A new {@code Expr} representing the number of microseconds since epoch. */ -export function timestampToUnixMicros(fieldName: string): FunctionExpr; -export function timestampToUnixMicros(expr: Expr | string): FunctionExpr { +export function timestampToUnixMicros(fieldName: string): FunctionExpression; +export function timestampToUnixMicros( + expr: Expression | string +): FunctionExpression { return fieldOrExpression(expr).timestampToUnixMicros(); } @@ -6422,7 +6335,7 @@ export function timestampToUnixMicros(expr: Expr | string): FunctionExpr { * @param expr The expression representing the number of milliseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMillisToTimestamp(expr: Expr): FunctionExpr; +export function unixMillisToTimestamp(expr: Expression): FunctionExpression; /** * @beta @@ -6438,8 +6351,10 @@ export function unixMillisToTimestamp(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the number of milliseconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixMillisToTimestamp(fieldName: string): FunctionExpr; -export function unixMillisToTimestamp(expr: Expr | string): FunctionExpr { +export function unixMillisToTimestamp(fieldName: string): FunctionExpression; +export function unixMillisToTimestamp( + expr: Expression | string +): FunctionExpression { const normalizedExpr = fieldOrExpression(expr); return normalizedExpr.unixMillisToTimestamp(); } @@ -6457,7 +6372,7 @@ export function unixMillisToTimestamp(expr: Expr | string): FunctionExpr { * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of milliseconds since epoch. */ -export function timestampToUnixMillis(expr: Expr): FunctionExpr; +export function timestampToUnixMillis(expr: Expression): FunctionExpression; /** * @beta @@ -6472,8 +6387,10 @@ export function timestampToUnixMillis(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the timestamp. * @return A new {@code Expr} representing the number of milliseconds since epoch. */ -export function timestampToUnixMillis(fieldName: string): FunctionExpr; -export function timestampToUnixMillis(expr: Expr | string): FunctionExpr { +export function timestampToUnixMillis(fieldName: string): FunctionExpression; +export function timestampToUnixMillis( + expr: Expression | string +): FunctionExpression { const normalizedExpr = fieldOrExpression(expr); return normalizedExpr.timestampToUnixMillis(); } @@ -6492,7 +6409,7 @@ export function timestampToUnixMillis(expr: Expr | string): FunctionExpr { * @param expr The expression representing the number of seconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixSecondsToTimestamp(expr: Expr): FunctionExpr; +export function unixSecondsToTimestamp(expr: Expression): FunctionExpression; /** * @beta @@ -6508,8 +6425,10 @@ export function unixSecondsToTimestamp(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the number of seconds since epoch. * @return A new {@code Expr} representing the timestamp. */ -export function unixSecondsToTimestamp(fieldName: string): FunctionExpr; -export function unixSecondsToTimestamp(expr: Expr | string): FunctionExpr { +export function unixSecondsToTimestamp(fieldName: string): FunctionExpression; +export function unixSecondsToTimestamp( + expr: Expression | string +): FunctionExpression { const normalizedExpr = fieldOrExpression(expr); return normalizedExpr.unixSecondsToTimestamp(); } @@ -6527,7 +6446,7 @@ export function unixSecondsToTimestamp(expr: Expr | string): FunctionExpr { * @param expr The expression representing the timestamp. * @return A new {@code Expr} representing the number of seconds since epoch. */ -export function timestampToUnixSeconds(expr: Expr): FunctionExpr; +export function timestampToUnixSeconds(expr: Expression): FunctionExpression; /** * @beta @@ -6542,8 +6461,10 @@ export function timestampToUnixSeconds(expr: Expr): FunctionExpr; * @param fieldName The name of the field representing the timestamp. * @return A new {@code Expr} representing the number of seconds since epoch. */ -export function timestampToUnixSeconds(fieldName: string): FunctionExpr; -export function timestampToUnixSeconds(expr: Expr | string): FunctionExpr { +export function timestampToUnixSeconds(fieldName: string): FunctionExpression; +export function timestampToUnixSeconds( + expr: Expression | string +): FunctionExpression { const normalizedExpr = fieldOrExpression(expr); return normalizedExpr.timestampToUnixSeconds(); } @@ -6564,10 +6485,10 @@ export function timestampToUnixSeconds(expr: Expr | string): FunctionExpr { * @return A new {@code Expr} representing the resulting timestamp. */ export function timestampAdd( - timestamp: Expr, - unit: Expr, - amount: Expr -): FunctionExpr; + timestamp: Expression, + unit: Expression, + amount: Expression +): FunctionExpression; /** * @beta @@ -6585,10 +6506,10 @@ export function timestampAdd( * @return A new {@code Expr} representing the resulting timestamp. */ export function timestampAdd( - timestamp: Expr, + timestamp: Expression, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6609,19 +6530,19 @@ export function timestampAdd( fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): FunctionExpr; +): FunctionExpression; export function timestampAdd( - timestamp: Expr | string, + timestamp: Expression | string, unit: - | Expr + | Expression | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: Expr | number -): FunctionExpr { + amount: Expression | number +): FunctionExpression { const normalizedTimestamp = fieldOrExpression(timestamp); const normalizedUnit = valueToDefaultExpr(unit); const normalizedAmount = valueToDefaultExpr(amount); @@ -6635,7 +6556,7 @@ export function timestampAdd( * * ```typescript * // Subtract some duration determined by field 'unit' and 'amount' from the 'timestamp' field. - * timestampSub(field("timestamp"), field("unit"), field("amount")); + * timestampSubtract(field("timestamp"), field("unit"), field("amount")); * ``` * * @param timestamp The expression representing the timestamp. @@ -6643,11 +6564,11 @@ export function timestampAdd( * @param amount The expression evaluates to amount of the unit. * @return A new {@code Expr} representing the resulting timestamp. */ -export function timestampSub( - timestamp: Expr, - unit: Expr, - amount: Expr -): FunctionExpr; +export function timestampSubtract( + timestamp: Expression, + unit: Expression, + amount: Expression +): FunctionExpression; /** * @beta @@ -6656,7 +6577,7 @@ export function timestampSub( * * ```typescript * // Subtract 1 day from the 'timestamp' field. - * timestampSub(field("timestamp"), "day", 1); + * timestampSubtract(field("timestamp"), "day", 1); * ``` * * @param timestamp The expression representing the timestamp. @@ -6664,11 +6585,11 @@ export function timestampSub( * @param amount The amount of time to subtract. * @return A new {@code Expr} representing the resulting timestamp. */ -export function timestampSub( - timestamp: Expr, +export function timestampSubtract( + timestamp: Expression, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): FunctionExpr; +): FunctionExpression; /** * @beta @@ -6677,7 +6598,7 @@ export function timestampSub( * * ```typescript * // Subtract 1 day from the 'timestamp' field. - * timestampSub("timestamp", "day", 1); + * timestampSubtract("timestamp", "day", 1); * ``` * * @param fieldName The name of the field representing the timestamp. @@ -6685,27 +6606,30 @@ export function timestampSub( * @param amount The amount of time to subtract. * @return A new {@code Expr} representing the resulting timestamp. */ -export function timestampSub( +export function timestampSubtract( fieldName: string, unit: 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', amount: number -): FunctionExpr; -export function timestampSub( - timestamp: Expr | string, +): FunctionExpression; +export function timestampSubtract( + timestamp: Expression | string, unit: - | Expr + | Expression | 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day', - amount: Expr | number -): FunctionExpr { + amount: Expression | number +): FunctionExpression { const normalizedTimestamp = fieldOrExpression(timestamp); const normalizedUnit = valueToDefaultExpr(unit); const normalizedAmount = valueToDefaultExpr(amount); - return normalizedTimestamp.timestampSub(normalizedUnit, normalizedAmount); + return normalizedTimestamp.timestampSubtract( + normalizedUnit, + normalizedAmount + ); } /** @@ -6716,7 +6640,7 @@ export function timestampSub( * ```typescript * // Check if the 'age' field is greater than 18 AND the 'city' field is "London" AND * // the 'status' field is "active" - * const condition = and(gt("age", 18), eq("city", "London"), eq("status", "active")); + * const condition = and(greaterThan("age", 18), equal("city", "London"), equal("status", "active")); * ``` * * @param first The first filter condition. @@ -6725,11 +6649,11 @@ export function timestampSub( * @return A new {@code Expr} representing the logical 'AND' operation. */ export function and( - first: BooleanExpr, - second: BooleanExpr, - ...more: BooleanExpr[] -): BooleanExpr { - return new BooleanExpr('and', [first, second, ...more], 'and'); + first: BooleanExpression, + second: BooleanExpression, + ...more: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression('and', [first, second, ...more], 'and'); } /** @@ -6740,7 +6664,7 @@ export function and( * ```typescript * // Check if the 'age' field is greater than 18 OR the 'city' field is "London" OR * // the 'status' field is "active" - * const condition = or(gt("age", 18), eq("city", "London"), eq("status", "active")); + * const condition = or(greaterThan("age", 18), equal("city", "London"), equal("status", "active")); * ``` * * @param first The first filter condition. @@ -6749,13 +6673,318 @@ export function and( * @return A new {@code Expr} representing the logical 'OR' operation. */ export function or( - first: BooleanExpr, - second: BooleanExpr, - ...more: BooleanExpr[] -): BooleanExpr { - return new BooleanExpr('or', [first, second, ...more], 'xor'); + first: BooleanExpression, + second: BooleanExpression, + ...more: BooleanExpression[] +): BooleanExpression { + return new BooleanExpression('or', [first, second, ...more], 'xor'); +} + +/** + * Creates an expression that returns the value of the base expression raised to the power of the exponent expression. + * + * ```typescript + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * pow(field("base"), field("exponent")); + * ``` + * + * @param base The expression to raise to the power of the exponent. + * @param exponent The expression to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: Expression, exponent: Expression): FunctionExpression; + +/** + * Creates an expression that returns the value of the base expression raised to the power of the exponent. + * + * ```typescript + * // Raise the value of the 'base' field to the power of 2. + * pow(field("base"), 2); + * ``` + * + * @param base The expression to raise to the power of the exponent. + * @param exponent The constant value to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: Expression, exponent: number): FunctionExpression; + +/** + * Creates an expression that returns the value of the base field raised to the power of the exponent expression. + * + * ```typescript + * // Raise the value of the 'base' field to the power of the 'exponent' field. + * pow("base", field("exponent")); + * ``` + * + * @param base The name of the field to raise to the power of the exponent. + * @param exponent The expression to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: string, exponent: Expression): FunctionExpression; + +/** + * Creates an expression that returns the value of the base field raised to the power of the exponent. + * + * ```typescript + * // Raise the value of the 'base' field to the power of 2. + * pow("base", 2); + * ``` + * + * @param base The name of the field to raise to the power of the exponent. + * @param exponent The constant value to raise the base to the power of. + * @return A new `Expr` representing the power operation. + */ +export function pow(base: string, exponent: number): FunctionExpression; +export function pow( + base: Expression | string, + exponent: Expression | number +): FunctionExpression { + return fieldOrExpression(base).pow(exponent as number); +} + +/** + * Creates an expression that rounds a numeric value to the nearest whole number. + * + * ```typescript + * // Round the value of the 'price' field. + * round("price"); + * ``` + * + * @param fieldName The name of the field to round. + * @return A new `Expr` representing the rounded value. + */ +export function round(fieldName: string): FunctionExpression; + +/** + * Creates an expression that rounds a numeric value to the nearest whole number. + * + * ```typescript + * // Round the value of the 'price' field. + * round(field("price")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which will be rounded. + * @return A new `Expr` representing the rounded value. + */ +export function round(expression: Expression): FunctionExpression; +export function round(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).round(); +} + +/** + * Creates an expression that returns the collection ID from a path. + * + * ```typescript + * // Get the collection ID from a path. + * collectionId("__name__"); + * ``` + * + * @param fieldName The name of the field to get the collection ID from. + * @return A new {@code Expr} representing the collectionId operation. + */ +export function collectionId(fieldName: string): FunctionExpression; + +/** + * Creates an expression that returns the collection ID from a path. + * + * ```typescript + * // Get the collection ID from a path. + * collectionId(field("__name__")); + * ``` + * + * @param expression An expression evaluating to a path, which the collection ID will be extracted from. + * @return A new {@code Expr} representing the collectionId operation. + */ +export function collectionId(expression: Expression): FunctionExpression; +export function collectionId(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).collectionId(); +} + +/** + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * length("name"); + * + * // Get the number of items in the 'cart' array. + * length("cart"); + * ``` + * + * @param fieldName The name of the field to calculate the length of. + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ +export function length(fieldName: string): FunctionExpression; + +/** + * Creates an expression that calculates the length of a string, array, map, vector, or bytes. + * + * ```typescript + * // Get the length of the 'name' field. + * length(field("name")); + * + * // Get the number of items in the 'cart' array. + * length(field("cart")); + * ``` + * + * @param expression An expression evaluating to a string, array, map, vector, or bytes, which the length will be calculated for. + * @return A new `Expr` representing the length of the string, array, map, vector, or bytes. + */ +export function length(expression: Expression): FunctionExpression; +export function length(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).length(); +} + +/** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * ln("value"); + * ``` + * + * @param fieldName The name of the field to compute the natural logarithm of. + * @return A new `Expr` representing the natural logarithm of the numeric value. + */ +export function ln(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the natural logarithm of a numeric value. + * + * ```typescript + * // Compute the natural logarithm of the 'value' field. + * ln(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the natural logarithm will be computed for. + * @return A new `Expr` representing the natural logarithm of the numeric value. + */ +export function ln(expression: Expression): FunctionExpression; +export function ln(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).ln(); } +/** + * Creates an expression that computes the logarithm of an expression to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with base 10. + * log(field("value"), 10); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the logarithm will be computed for. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(expression: Expression, base: number): FunctionExpression; +/** + * Creates an expression that computes the logarithm of an expression to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with the base in the 'base' field. + * log(field("value"), field("base")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the logarithm will be computed for. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log( + expression: Expression, + base: Expression +): FunctionExpression; +/** + * Creates an expression that computes the logarithm of a field to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with base 10. + * log("value", 10); + * ``` + * + * @param fieldName The name of the field to compute the logarithm of. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(fieldName: string, base: number): FunctionExpression; +/** + * Creates an expression that computes the logarithm of a field to a given base. + * + * ```typescript + * // Compute the logarithm of the 'value' field with the base in the 'base' field. + * log("value", field("base")); + * ``` + * + * @param fieldName The name of the field to compute the logarithm of. + * @param base The base of the logarithm. + * @return A new {@code Expr} representing the logarithm of the numeric value. + */ +export function log(fieldName: string, base: Expression): FunctionExpression; +export function log( + expr: Expression | string, + base: number | Expression +): FunctionExpression { + return fieldOrExpression(expr).log(valueToDefaultExpr(base)); +} + +/** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * sqrt(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the square root will be computed for. + * @return A new {@code Expr} representing the square root of the numeric value. + */ +export function sqrt(expression: Expression): FunctionExpression; +/** + * Creates an expression that computes the square root of a numeric value. + * + * ```typescript + * // Compute the square root of the 'value' field. + * sqrt("value"); + * ``` + * + * @param fieldName The name of the field to compute the square root of. + * @return A new {@code Expr} representing the square root of the numeric value. + */ +export function sqrt(fieldName: string): FunctionExpression; +export function sqrt(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).sqrt(); +} + +/** + * Creates an expression that reverses a string. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * strReverse(field("myString")); + * ``` + * + * @param stringExpression An expression evaluating to a string value, which will be reversed. + * @return A new {@code Expr} representing the reversed string. + */ +export function stringReverse(stringExpression: Expression): FunctionExpression; + +/** + * Creates an expression that reverses a string value in the specified field. + * + * ```typescript + * // Reverse the value of the 'myString' field. + * strReverse("myString"); + * ``` + * + * @param field The name of the field representing the string to reverse. + * @return A new {@code Expr} representing the reversed string. + */ +export function stringReverse(field: string): FunctionExpression; +export function stringReverse(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).stringReverse(); +} + +// TODO(new-expression): Add new top-level expression function definitions above this line + /** * @beta * @@ -6770,7 +6999,7 @@ export function or( * @param expr The expression to create an ascending ordering for. * @return A new `Ordering` for ascending sorting. */ -export function ascending(expr: Expr): Ordering; +export function ascending(expr: Expression): Ordering; /** * @beta @@ -6787,7 +7016,7 @@ export function ascending(expr: Expr): Ordering; * @return A new `Ordering` for ascending sorting. */ export function ascending(fieldName: string): Ordering; -export function ascending(field: Expr | string): Ordering { +export function ascending(field: Expression | string): Ordering { return new Ordering(fieldOrExpression(field), 'ascending', 'ascending'); } @@ -6805,7 +7034,7 @@ export function ascending(field: Expr | string): Ordering { * @param expr The expression to create a descending ordering for. * @return A new `Ordering` for descending sorting. */ -export function descending(expr: Expr): Ordering; +export function descending(expr: Expression): Ordering; /** * @beta @@ -6822,7 +7051,7 @@ export function descending(expr: Expr): Ordering; * @return A new `Ordering` for descending sorting. */ export function descending(fieldName: string): Ordering; -export function descending(field: Expr | string): Ordering { +export function descending(field: Expression | string): Ordering { return new Ordering(fieldOrExpression(field), 'descending', 'descending'); } @@ -6835,7 +7064,7 @@ export function descending(field: Expr | string): Ordering { */ export class Ordering implements ProtoValueSerializable, UserData { constructor( - readonly expr: Expr, + readonly expr: Expression, readonly direction: 'ascending' | 'descending', readonly _methodName: string | undefined ) {} diff --git a/packages/firestore/src/lite-api/pipeline-result.ts b/packages/firestore/src/lite-api/pipeline-result.ts index 635636ac46b..901af2949f9 100644 --- a/packages/firestore/src/lite-api/pipeline-result.ts +++ b/packages/firestore/src/lite-api/pipeline-result.ts @@ -40,14 +40,6 @@ export class PipelineSnapshot { 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; @@ -95,7 +87,7 @@ export class PipelineResult { * @internal * @private */ - readonly _fields: ObjectValue | undefined; + readonly _fields: ObjectValue; /** * @private @@ -104,16 +96,14 @@ export class PipelineResult { * @param userDataWriter The serializer used to encode/decode protobuf. * @param ref The reference to the document. * @param fields The fields of the Firestore `Document` Protobuf backing - * this document (or undefined if the document does not exist). - * @param readTime The time when this result was read (or undefined if - * the document exists only locally). + * this document. * @param createTime The time when the document was created if the result is a document, undefined otherwise. * @param updateTime The time when the document was last updated if the result is a document, undefined otherwise. */ constructor( userDataWriter: AbstractUserDataWriter, + fields: ObjectValue, ref?: DocumentReference, - fields?: ObjectValue, createTime?: Timestamp, updateTime?: Timestamp ) { @@ -164,10 +154,9 @@ export class PipelineResult { } /** - * Retrieves all fields in the result as an object. Returns 'undefined' if - * the document doesn't exist. + * Retrieves all fields in the result as an object. * - * @returns {T|undefined} An object containing all fields in the document or + * @returns {T} An object containing all fields in the document or * 'undefined' if the document doesn't exist. * * @example @@ -180,11 +169,7 @@ export class PipelineResult { * }); * ``` */ - data(): AppModelType | undefined { - if (this._fields === undefined) { - return undefined; - } - + data(): AppModelType { return this._userDataWriter.convertValue( this._fields.value ) as AppModelType; diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts index de28ceecca6..40624ccfac4 100644 --- a/packages/firestore/src/lite-api/pipeline-source.ts +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -15,13 +15,13 @@ * limitations under the License. */ -import {DatabaseId} from '../core/database_info'; -import {toPipeline} from '../core/pipeline-util'; -import {Code, FirestoreError} from '../util/error'; -import {isCollectionReference, isString} from "../util/types"; +import { DatabaseId } from '../core/database_info'; +import { toPipeline } from '../core/pipeline-util'; +import { Code, FirestoreError } from '../util/error'; +import { isCollectionReference, isString } from '../util/types'; -import {Pipeline} from './pipeline'; -import {CollectionReference, DocumentReference, Query} from './reference'; +import { Pipeline } from './pipeline'; +import { CollectionReference, DocumentReference, Query } from './reference'; import { CollectionGroupSource, CollectionSource, @@ -34,8 +34,8 @@ import { CollectionStageOptions, DatabaseStageOptions, DocumentsStageOptions -} from "./stage_options"; -import {UserDataReader, UserDataSource} from "./user_data_reader"; +} from './stage_options'; +import { UserDataReader, UserDataSource } from './user_data_reader'; /** * Represents the source of a Firestore {@link Pipeline}. @@ -70,10 +70,7 @@ export class PipelineSource { */ collection(options: CollectionStageOptions): PipelineType; collection( - collectionOrOptions: - | string - | CollectionReference - | CollectionStageOptions + collectionOrOptions: string | CollectionReference | CollectionStageOptions ): PipelineType { // Process argument union(s) from method overloads const options = @@ -98,13 +95,14 @@ export class PipelineSource { : collectionRefOrString.path; // Create stage object - const stage = new CollectionSource( - normalizedCollection, - options); + const stage = new CollectionSource(normalizedCollection, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'collection'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'collection' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -120,34 +118,29 @@ export class PipelineSource { * Returns all documents from a collection ID regardless of the parent. * @param options - Options defining how this CollectionGroupStage is evaluated. */ + collectionGroup(options: CollectionGroupStageOptions): PipelineType; collectionGroup( - options: CollectionGroupStageOptions - ): PipelineType; - collectionGroup( - collectionIdOrOptions: - | string - | CollectionGroupStageOptions + collectionIdOrOptions: string | CollectionGroupStageOptions ): PipelineType { // Process argument union(s) from method overloads let collectionId: string; let options: {}; - if (isString( - collectionIdOrOptions - )) { + if (isString(collectionIdOrOptions)) { collectionId = collectionIdOrOptions; options = {}; } else { - ({collectionId, ...options} = collectionIdOrOptions); + ({ collectionId, ...options } = collectionIdOrOptions); } // Create stage object - const stage = new CollectionGroupSource( - collectionId, - options); + const stage = new CollectionGroupSource(collectionId, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'collectionGroup'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'collectionGroup' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -172,7 +165,10 @@ export class PipelineSource { // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'database'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'database' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -198,9 +194,7 @@ export class PipelineSource { */ documents(options: DocumentsStageOptions): PipelineType; documents( - docsOrOptions: - | Array - | DocumentsStageOptions + docsOrOptions: Array | DocumentsStageOptions ): PipelineType { // Process argument union(s) from method overloads let options: {}; @@ -209,15 +203,13 @@ export class PipelineSource { docs = docsOrOptions; options = {}; } else { - ({docs, ...options} = docsOrOptions); + ({ docs, ...options } = docsOrOptions); } // Validate that all user provided references are for the same Firestore DB docs .filter(v => v instanceof DocumentReference) - .forEach(dr => - this._validateReference(dr as DocumentReference) - ); + .forEach(dr => this._validateReference(dr as DocumentReference)); // Convert user land convenience types to internal types const normalizedDocs: string[] = docs.map(doc => @@ -225,13 +217,14 @@ export class PipelineSource { ); // Create stage object - const stage = new DocumentsSource( - normalizedDocs, - options); + const stage = new DocumentsSource(normalizedDocs, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'documents'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'documents' + ); stage._readUserData(parseContext); // Add stage to the pipeline diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts index 4f2522320a6..51edd6c2dd4 100644 --- a/packages/firestore/src/lite-api/pipeline.ts +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -21,39 +21,46 @@ import { Pipeline as ProtoPipeline, Stage as ProtoStage } from '../protos/firestore_proto_api'; -import {JsonProtoSerializer, ProtoSerializable} from '../remote/serializer'; -import {isPlainObject} from '../util/input_validation'; +import { JsonProtoSerializer, ProtoSerializable } from '../remote/serializer'; +import { isPlainObject } from '../util/input_validation'; import { - aliasedAggregateToMap, fieldOrExpression, + aliasedAggregateToMap, + fieldOrExpression, selectablesToMap, vectorToExpr -} from "../util/pipeline_util"; +} from '../util/pipeline_util'; import { isAliasedAggregate, - isBooleanExpr, isExpr, - isField, isLitePipeline, isOrdering, + isBooleanExpr, + isExpr, + isField, + isLitePipeline, + isOrdering, isSelectable, - isString, toField -} from "../util/types"; + isString, + toField +} from '../util/types'; -import {Firestore} from './database'; +import { Firestore } from './database'; import { _mapValue, AggregateFunction, AggregateWithAlias, - BooleanExpr, _constant, - Expr, + BooleanExpression, + _constant, + Expression, Field, field, Ordering, - Selectable, _field + Selectable, + _field } from './expressions'; import { AddFields, Aggregate, Distinct, FindNearest, - GenericStage, + RawStage, Limit, Offset, RemoveFields, @@ -73,17 +80,18 @@ import { FindNearestStageOptions, LimitStageOptions, OffsetStageOptions, - RemoveFieldsStageOptions, ReplaceWithStageOptions, SampleStageOptions, + RemoveFieldsStageOptions, + ReplaceWithStageOptions, + SampleStageOptions, SelectStageOptions, - SortStageOptions, StageOptions, UnionStageOptions, UnnestStageOptions, + SortStageOptions, + StageOptions, + UnionStageOptions, + UnnestStageOptions, WhereStageOptions -} from "./stage_options"; -import { - UserDataReader, - UserDataSource -} from './user_data_reader'; -import {AbstractUserDataWriter} from './user_data_writer'; - +} from './stage_options'; +import { UserDataReader, UserDataSource } from './user_data_reader'; +import { AbstractUserDataWriter } from './user_data_writer'; /** * @beta @@ -124,10 +132,6 @@ import {AbstractUserDataWriter} from './user_data_writer'; * .aggregate(avg(field("rating")).as("averageRating"))); * ``` */ - -/** - * Base-class implementation - */ export class Pipeline implements ProtoSerializable { /** * @internal @@ -162,8 +166,8 @@ export class Pipeline implements ProtoSerializable { * The added fields are defined using {@link Selectable}s, which can be: * * - {@link Field}: References an existing document field. - * - {@link Expr}: Either a literal value (see {@link Constant}) or a computed value - * (see {@FunctionExpr}) with an assigned alias using {@link Expr#as}. + * - {@link Expression}: Either a literal value (see {@link Constant}) or a computed value + * (see {@FunctionExpr}) with an assigned alias using {@link Expression#as}. * * Example: * @@ -179,10 +183,7 @@ export class Pipeline implements ProtoSerializable { * @param additionalFields Optional additional fields to add to the documents, specified as {@link Selectable}s. * @return A new Pipeline object with this stage appended to the stage list. */ - addFields( - field: Selectable, - ...additionalFields: Selectable[] - ): Pipeline; + addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline; /** * Adds new fields to outputs from previous stages. * @@ -193,8 +194,8 @@ export class Pipeline implements ProtoSerializable { * The added fields are defined using {@link Selectable}s, which can be: * * - {@link Field}: References an existing document field. - * - {@link Expr}: Either a literal value (see {@link Constant}) or a computed value - * (see {@FunctionExpr}) with an assigned alias using {@link Expr#as}. + * - {@link Expression}: Either a literal value (see {@link Constant}) or a computed value + * (see {@FunctionExpr}) with an assigned alias using {@link Expression#as}. * * Example: * @@ -211,9 +212,7 @@ export class Pipeline implements ProtoSerializable { */ addFields(options: AddFieldsStageOptions): Pipeline; addFields( - fieldOrOptions: - | Selectable - | AddFieldsStageOptions, + fieldOrOptions: Selectable | AddFieldsStageOptions, ...additionalFields: Selectable[] ): Pipeline { // Process argument union(s) from method overloads @@ -223,20 +222,21 @@ export class Pipeline implements ProtoSerializable { fields = [fieldOrOptions, ...additionalFields]; options = {}; } else { - ({fields, ...options} = fieldOrOptions); + ({ fields, ...options } = fieldOrOptions); } // Convert user land convenience types to internal types - const normalizedFields: Map = selectablesToMap(fields); + const normalizedFields: Map = selectablesToMap(fields); // Create stage object - const stage = new AddFields( - normalizedFields, - options); + const stage = new AddFields(normalizedFields, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'addFields'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'addFields' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -284,10 +284,7 @@ export class Pipeline implements ProtoSerializable { */ removeFields(options: RemoveFieldsStageOptions): Pipeline; removeFields( - fieldValueOrOptions: - | Field - | string - | RemoveFieldsStageOptions, + fieldValueOrOptions: Field | string | RemoveFieldsStageOptions, ...additionalFields: Array ): Pipeline { // Process argument union(s) from method overloads @@ -310,11 +307,12 @@ export class Pipeline implements ProtoSerializable { // User data must be read in the context of the API method to // provide contextual errors - stage._readUserData( this.userDataReader.createContext(UserDataSource.Argument, 'removeFields')); + stage._readUserData( + this.userDataReader.createContext(UserDataSource.Argument, 'removeFields') + ); // Add stage to the pipeline return this._addStage(stage); - } /** @@ -326,7 +324,7 @@ export class Pipeline implements ProtoSerializable { *

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

    If no selections are provided, the output of this stage is empty. Use {@link @@ -363,7 +361,7 @@ export class Pipeline implements ProtoSerializable { *

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

    If no selections are provided, the output of this stage is empty. Use {@link @@ -386,10 +384,7 @@ export class Pipeline implements ProtoSerializable { */ select(options: SelectStageOptions): Pipeline; select( - selectionOrOptions: - | Selectable - | string - | SelectStageOptions, + selectionOrOptions: Selectable | string | SelectStageOptions, ...additionalSelections: Array ): Pipeline { // Process argument union(s) from method overloads @@ -404,7 +399,7 @@ export class Pipeline implements ProtoSerializable { : selectionOrOptions.selections; // Convert user land convenience types to internal types - const normalizedSelections: Map = + const normalizedSelections: Map = selectablesToMap(selections); // Create stage object @@ -412,7 +407,10 @@ export class Pipeline implements ProtoSerializable { // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'select'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'select' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -421,11 +419,11 @@ export class Pipeline implements ProtoSerializable { /** * Filters the documents from previous stages to only include those matching the specified {@link - * BooleanExpr}. + * BooleanExpression}. * *

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

      *
    • field comparators: {@link Function#eq}, {@link Function#lt} (less than), {@link @@ -447,17 +445,17 @@ export class Pipeline implements ProtoSerializable { * ); * ``` * - * @param condition The {@link BooleanExpr} to apply. + * @param condition The {@link BooleanExpression} to apply. * @return A new Pipeline object with this stage appended to the stage list. */ - where(condition: BooleanExpr): Pipeline; + where(condition: BooleanExpression): Pipeline; /** * Filters the documents from previous stages to only include those matching the specified {@link - * BooleanExpr}. + * BooleanExpression}. * *

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

        *
      • field comparators: {@link Function#eq}, {@link Function#lt} (less than), {@link @@ -483,27 +481,22 @@ export class Pipeline implements ProtoSerializable { * @return A new Pipeline object with this stage appended to the stage list. */ where(options: WhereStageOptions): Pipeline; - where( - conditionOrOptions: - | BooleanExpr - | WhereStageOptions - ): Pipeline { + where(conditionOrOptions: BooleanExpression | WhereStageOptions): Pipeline { // Process argument union(s) from method overloads const options = isBooleanExpr(conditionOrOptions) ? {} : conditionOrOptions; - const condition: BooleanExpr = isBooleanExpr( - conditionOrOptions - ) + const condition: BooleanExpression = isBooleanExpr(conditionOrOptions) ? conditionOrOptions : conditionOrOptions.condition; // Create stage object - const stage = new Where( - condition, - options); + const stage = new Where(condition, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'where'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'where' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -552,9 +545,7 @@ export class Pipeline implements ProtoSerializable { * @return A new Pipeline object with this stage appended to the stage list. */ offset(options: OffsetStageOptions): Pipeline; - offset( - offsetOrOptions: number | OffsetStageOptions - ): Pipeline { + offset(offsetOrOptions: number | OffsetStageOptions): Pipeline { // Process argument union(s) from method overloads const options = isNumber(offsetOrOptions) ? {} : offsetOrOptions; const offset: number = isNumber(offsetOrOptions) @@ -562,13 +553,14 @@ export class Pipeline implements ProtoSerializable { : offsetOrOptions.offset; // Create stage object - const stage = new Offset( - offset, - options); + const stage = new Offset(offset, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'offset'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'offset' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -627,9 +619,7 @@ export class Pipeline implements ProtoSerializable { * @return A new Pipeline object with this stage appended to the stage list. */ limit(options: LimitStageOptions): Pipeline; - limit( - limitOrOptions: number | LimitStageOptions - ): Pipeline { + limit(limitOrOptions: number | LimitStageOptions): Pipeline { // Process argument union(s) from method overloads const options = isNumber(limitOrOptions) ? {} : limitOrOptions; const limit: number = isNumber(limitOrOptions) @@ -637,13 +627,14 @@ export class Pipeline implements ProtoSerializable { : limitOrOptions.limit; // Create stage object - const stage = new Limit( - limit, - options); + const stage = new Limit(limit, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'limit'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'limit' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -654,14 +645,14 @@ export class Pipeline implements ProtoSerializable { * Returns a set of distinct values from the inputs to this stage. * * This stage runs through the results from previous stages to include only results with - * unique combinations of {@link Expr} values ({@link Field}, {@link Function}, etc). + * unique combinations of {@link Expression} values ({@link Field}, {@link Function}, etc). * * The parameters to this stage are defined using {@link Selectable} expressions or strings: * * - {@code string}: Name of an existing field * - {@link Field}: References an existing document field. * - {@link AliasedExpr}: Represents the result of a function with an assigned alias name - * using {@link Expr#as}. + * using {@link Expression#as}. * * Example: * @@ -686,14 +677,14 @@ export class Pipeline implements ProtoSerializable { * Returns a set of distinct values from the inputs to this stage. * * This stage runs through the results from previous stages to include only results with - * unique combinations of {@link Expr} values ({@link Field}, {@link Function}, etc). + * unique combinations of {@link Expression} values ({@link Field}, {@link Function}, etc). * * The parameters to this stage are defined using {@link Selectable} expressions or strings: * * - {@code string}: Name of an existing field * - {@link Field}: References an existing document field. * - {@link AliasedExpr}: Represents the result of a function with an assigned alias name - * using {@link Expr#as}. + * using {@link Expression#as}. * * Example: * @@ -709,10 +700,7 @@ export class Pipeline implements ProtoSerializable { */ distinct(options: DistinctStageOptions): Pipeline; distinct( - groupOrOptions: - | string - | Selectable - | DistinctStageOptions, + groupOrOptions: string | Selectable | DistinctStageOptions, ...additionalGroups: Array ): Pipeline { // Process argument union(s) from method overloads @@ -726,16 +714,17 @@ export class Pipeline implements ProtoSerializable { : groupOrOptions.groups; // Convert user land convenience types to internal types - const convertedGroups: Map = selectablesToMap(groups); + const convertedGroups: Map = selectablesToMap(groups); // Create stage object - const stage = new Distinct( - convertedGroups, - options); + const stage = new Distinct(convertedGroups, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'distinct'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'distinct' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -747,7 +736,7 @@ export class Pipeline implements ProtoSerializable { * *

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

        Example: * @@ -783,7 +772,7 @@ export class Pipeline implements ProtoSerializable { * specifying groups is the same as putting the entire inputs into one group.

      • *
      • **Accumulators:** One or more accumulation operations to perform within each group. These * are defined using {@link AggregateWithAlias} expressions, which are typically created by - * calling {@link Expr#as} on {@link AggregateFunction} instances. Each aggregation + * calling {@link Expression#as} on {@link AggregateFunction} instances. Each aggregation * calculates a value (e.g., sum, average, count) based on the documents within its group.
      • *
      * @@ -804,34 +793,40 @@ export class Pipeline implements ProtoSerializable { */ aggregate(options: AggregateStageOptions): Pipeline; aggregate( - targetOrOptions: - | AggregateWithAlias - | AggregateStageOptions, + targetOrOptions: AggregateWithAlias | AggregateStageOptions, ...rest: AggregateWithAlias[] ): Pipeline { // Process argument union(s) from method overloads const options = isAliasedAggregate(targetOrOptions) ? {} : targetOrOptions; - const accumulators: AggregateWithAlias[] = - isAliasedAggregate(targetOrOptions) - ? [targetOrOptions, ...rest] - : targetOrOptions.accumulators; - const groups: Array = - isAliasedAggregate(targetOrOptions) ? [] : targetOrOptions.groups ?? []; + const accumulators: AggregateWithAlias[] = isAliasedAggregate( + targetOrOptions + ) + ? [targetOrOptions, ...rest] + : targetOrOptions.accumulators; + const groups: Array = isAliasedAggregate( + targetOrOptions + ) + ? [] + : targetOrOptions.groups ?? []; // Convert user land convenience types to internal types const convertedAccumulators: Map = aliasedAggregateToMap(accumulators); - const convertedGroups: Map = selectablesToMap(groups); + const convertedGroups: Map = selectablesToMap(groups); // Create stage object const stage = new Aggregate( convertedGroups, convertedAccumulators, - options); + options + ); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'aggregate'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'aggregate' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -881,11 +876,15 @@ export class Pipeline implements ProtoSerializable { vectorValue, field, options.distanceMeasure, - internalOptions); + internalOptions + ); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'addFields'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'addFields' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -917,10 +916,7 @@ export class Pipeline implements ProtoSerializable { * @param additionalOrderings Optional additional {@link Ordering} instances specifying the additional sorting criteria. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - sort( - ordering: Ordering, - ...additionalOrderings: Ordering[] - ): Pipeline; + sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline; /** * Sorts the documents from previous stages based on one or more {@link Ordering} criteria. * @@ -947,27 +943,24 @@ export class Pipeline implements ProtoSerializable { */ sort(options: SortStageOptions): Pipeline; sort( - orderingOrOptions: - | Ordering - | SortStageOptions, + orderingOrOptions: Ordering | SortStageOptions, ...additionalOrderings: Ordering[] ): Pipeline { // Process argument union(s) from method overloads const options = isOrdering(orderingOrOptions) ? {} : orderingOrOptions; - const orderings: Ordering[] = isOrdering( - orderingOrOptions - ) + const orderings: Ordering[] = isOrdering(orderingOrOptions) ? [orderingOrOptions, ...additionalOrderings] : orderingOrOptions.orderings; // Create stage object - const stage = new Sort( - orderings, - options); + const stage = new Sort(orderings, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'sort'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'sort' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -1039,10 +1032,10 @@ export class Pipeline implements ProtoSerializable { * // } * ``` * - * @param expr An {@link Expr} that when returned evaluates to a map. + * @param expr An {@link Expression} that when returned evaluates to a map. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - replaceWith(expr: Expr): Pipeline; + replaceWith(expr: Expression): Pipeline; /** * Fully overwrites all fields in a document with those coming from a map. * @@ -1081,15 +1074,12 @@ export class Pipeline implements ProtoSerializable { */ replaceWith(options: ReplaceWithStageOptions): Pipeline; replaceWith( - valueOrOptions: - | Expr - | string - | ReplaceWithStageOptions + valueOrOptions: Expression | string | ReplaceWithStageOptions ): Pipeline { // Process argument union(s) from method overloads const options = isString(valueOrOptions) || isExpr(valueOrOptions) ? {} : valueOrOptions; - const fieldNameOrExpr: string | Expr = + const fieldNameOrExpr: string | Expression = isString(valueOrOptions) || isExpr(valueOrOptions) ? valueOrOptions : valueOrOptions.map; @@ -1098,13 +1088,14 @@ export class Pipeline implements ProtoSerializable { const mapExpr = fieldOrExpression(fieldNameOrExpr); // Create stage object - const stage = new Replace( - mapExpr, - options); + const stage = new Replace(mapExpr, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'replaceWith'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'replaceWith' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -1150,9 +1141,7 @@ export class Pipeline implements ProtoSerializable { * @return A new {@code Pipeline} object with this stage appended to the stage list. */ sample(options: SampleStageOptions): Pipeline; - sample( - documentsOrOptions: number | SampleStageOptions - ): Pipeline { + sample(documentsOrOptions: number | SampleStageOptions): Pipeline { // Process argument union(s) from method overloads const options = isNumber(documentsOrOptions) ? {} : documentsOrOptions; let rate: number; @@ -1169,14 +1158,14 @@ export class Pipeline implements ProtoSerializable { } // Create stage object - const stage = new Sample( - rate, - mode, - options); + const stage = new Sample(rate, mode, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'sample'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'sample' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -1221,11 +1210,7 @@ export class Pipeline implements ProtoSerializable { * @return A new {@code Pipeline} object with this stage appended to the stage list. */ union(options: UnionStageOptions): Pipeline; - union( - otherOrOptions: - | Pipeline - | UnionStageOptions - ): Pipeline { + union(otherOrOptions: Pipeline | UnionStageOptions): Pipeline { // Process argument union(s) from method overloads let options: {}; let otherPipeline: Pipeline; @@ -1233,17 +1218,18 @@ export class Pipeline implements ProtoSerializable { options = {}; otherPipeline = otherOrOptions; } else { - ({other: otherPipeline, ...options} = otherOrOptions); + ({ other: otherPipeline, ...options } = otherOrOptions); } // Create stage object - const stage = new Union( - otherPipeline, - options); + const stage = new Union(otherPipeline, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'union'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'union' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -1270,20 +1256,19 @@ export class Pipeline implements ProtoSerializable { * * // Emit a book document for each tag of the book. * firestore.pipeline().collection("books") - * .unnest(field("tags").as('tag')); + * .unnest(field("tags").as('tag'), 'tagIndex'); * * // Output: - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", ... } - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", ... } - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", "tagIndex": 0, ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", "tagIndex": 1, ... } + * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", "tagIndex": 2, ... } * ``` * * @param selectable A selectable expression defining the field to unnest and the alias to use for each un-nested element in the output documents. + * @param indexField An optional string value specifying the field path to write the offset (starting at zero) into the array the un-nested element is from * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - unnest( - selectable: Selectable - ): Pipeline; + unnest(selectable: Selectable, indexField?: string): Pipeline; /** * Produces a document for each element in an input array. * @@ -1317,36 +1302,41 @@ export class Pipeline implements ProtoSerializable { */ unnest(options: UnnestStageOptions): Pipeline; unnest( - selectableOrOptions: - | Selectable - | UnnestStageOptions + selectableOrOptions: Selectable | UnnestStageOptions, + indexField?: string ): Pipeline { // Process argument union(s) from method overloads - let options: {indexField?: string | Field} & StageOptions; + let options: { indexField?: Field } & StageOptions; let selectable: Selectable; + let indexFieldName: string | undefined; if (isSelectable(selectableOrOptions)) { options = {}; selectable = selectableOrOptions; + indexFieldName = indexField; } else { - ({selectable, ...options} = selectableOrOptions); + ({ + selectable, + indexField: indexFieldName, + ...options + } = selectableOrOptions); } // Convert user land convenience types to internal types const alias = selectable.alias; - const expr = selectable.expr as Expr; - if (isString(options['indexField'])) { - options.indexField = _field(options.indexField, 'unnest'); + const expr = selectable.expr as Expression; + if (isString(indexFieldName)) { + options.indexField = _field(indexFieldName, 'unnest'); } // Create stage object - const stage = new Unnest( - alias, - expr, - options); + const stage = new Unnest(alias, expr, options); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'unnest'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'unnest' + ); stage._readUserData(parseContext); // Add stage to the pipeline @@ -1377,11 +1367,11 @@ export class Pipeline implements ProtoSerializable { rawStage( name: string, params: unknown[], - options?: {[key: string]: Expr | unknown} + options?: { [key: string]: Expression | unknown } ): Pipeline { // Convert user land convenience types to internal types const expressionParams = params.map((value: unknown) => { - if (value instanceof Expr) { + if (value instanceof Expression) { return value; } else if (value instanceof AggregateFunction) { return value; @@ -1393,14 +1383,14 @@ export class Pipeline implements ProtoSerializable { }); // Create stage object - const stage = new GenericStage( - name, - expressionParams, - options ?? {}); + const stage = new RawStage(name, expressionParams, options ?? {}); // User data must be read in the context of the API method to // provide contextual errors - const parseContext = this.userDataReader.createContext(UserDataSource.Argument, 'rawStage'); + const parseContext = this.userDataReader.createContext( + UserDataSource.Argument, + 'rawStage' + ); stage._readUserData(parseContext); // Add stage to the pipeline diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts index 1d8e0297122..397e27bc1b4 100644 --- a/packages/firestore/src/lite-api/pipeline_impl.ts +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -18,7 +18,7 @@ import { StructuredPipeline, StructuredPipelineOptions -} from "../core/structured_pipeline"; +} from '../core/structured_pipeline'; import { invokeExecutePipeline } from '../remote/datastore'; import { getDatastore } from './components'; @@ -107,10 +107,10 @@ export function execute(pipeline: Pipeline): Promise { element => new PipelineResult( pipeline._userDataWriter, + element.fields!, element.key?.path ? new DocumentReference(pipeline._db, null, element.key) : undefined, - element.fields, element.createTime?.toTimestamp(), element.updateTime?.toTimestamp() ) @@ -123,7 +123,11 @@ export function execute(pipeline: Pipeline): Promise { Firestore.prototype.pipeline = function (): PipelineSource { const userDataWriter = new LiteUserDataWriter(this); const userDataReader = newUserDataReader(this); - return new PipelineSource(this._databaseId, userDataReader, (stages: Stage[]) => { - return new Pipeline(this, userDataReader, userDataWriter, stages); - }); + return new PipelineSource( + this._databaseId, + userDataReader, + (stages: Stage[]) => { + return new Pipeline(this, userDataReader, userDataWriter, stages); + } + ); }; diff --git a/packages/firestore/src/lite-api/pipeline_options.ts b/packages/firestore/src/lite-api/pipeline_options.ts index 0df2676f144..1ab8ead510f 100644 --- a/packages/firestore/src/lite-api/pipeline_options.ts +++ b/packages/firestore/src/lite-api/pipeline_options.ts @@ -16,7 +16,7 @@ import type { Pipeline } from './pipeline'; /** * Options defining Pipeline execution. */ -export interface PipelineOptions { +export interface PipelineExecuteOptions { /** * Pipeline to be evaluated. */ @@ -38,14 +38,14 @@ export interface PipelineOptions { * by Firestore (for example: string, boolean, number, map, …). Value types * not known to the SDK will be rejected. * - * Values specified in customOptions will take precedence over any options + * Values specified in rawOptions will take precedence over any options * with the same name set by the SDK. * * Override the `example_option`: * ``` * execute({ * pipeline: myPipeline, - * customOptions: { + * rawOptions: { * // Override `example_option`. This will not * // merge with the existing `example_option` object. * "example_option": { @@ -55,12 +55,12 @@ export interface PipelineOptions { * } * ``` * - * `customOptions` supports dot notation, if you want to override + * `rawOptions` supports dot notation, if you want to override * a nested option. * ``` * execute({ * pipeline: myPipeline, - * customOptions: { + * rawOptions: { * // Override `example_option.foo` and do not override * // any other properties of `example_option`. * "example_option.foo": "bar" @@ -68,7 +68,7 @@ export interface PipelineOptions { * } * ``` */ - customOptions?: { + rawOptions?: { [name: string]: unknown; }; } diff --git a/packages/firestore/src/lite-api/snapshot.ts b/packages/firestore/src/lite-api/snapshot.ts index 66c3a1422e9..024d26dd22e 100644 --- a/packages/firestore/src/lite-api/snapshot.ts +++ b/packages/firestore/src/lite-api/snapshot.ts @@ -523,7 +523,7 @@ export function fieldPathFromArgument( } else if (arg instanceof FieldPath) { return arg._internalPath; } else if (arg instanceof Field) { - return fieldPathFromDotSeparatedString(methodName, arg.fieldName()); + 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 35376f07d4b..ac726ae2a47 100644 --- a/packages/firestore/src/lite-api/stage.ts +++ b/packages/firestore/src/lite-api/stage.ts @@ -15,38 +15,34 @@ * limitations under the License. */ -import {ParseContext} from "../api/parse_context"; -import { OptionsUtil} from "../core/options_util"; +import { ParseContext } from '../api/parse_context'; +import { OptionsUtil } from '../core/options_util'; import { - ApiClientObjectMap, firestoreV1ApiClientInterfaces, + ApiClientObjectMap, + firestoreV1ApiClientInterfaces, Stage as ProtoStage } from '../protos/firestore_proto_api'; -import {toNumber} from '../remote/number_serializer'; +import { toNumber } from '../remote/number_serializer'; import { JsonProtoSerializer, ProtoSerializable, toMapValue, toPipelineValue, - toStringValue, + toStringValue } from '../remote/serializer'; -import {hardAssert} from '../util/assert'; +import { hardAssert } from '../util/assert'; import { AggregateFunction, - BooleanExpr, - Expr, + BooleanExpression, + Expression, Field, field, Ordering } from './expressions'; -import {Pipeline} from './pipeline'; -import { StageOptions -} from "./stage_options"; -import { - isUserData, - UserData -} from "./user_data_reader"; - +import { Pipeline } from './pipeline'; +import { StageOptions } from './stage_options'; +import { isUserData, UserData } from './user_data_reader'; import Value = firestoreV1ApiClientInterfaces.Value; @@ -65,11 +61,15 @@ export abstract class Stage implements ProtoSerializable, UserData { protected rawOptions?: Record; constructor(options: StageOptions) { - ({rawOptions: this.rawOptions, ...this.knownOptions} = options); + ({ rawOptions: this.rawOptions, ...this.knownOptions } = options); } _readUserData(context: ParseContext): void { - this.optionsProto = this._optionsUtil.getOptionsProto(context, this.knownOptions, this.rawOptions); + this.optionsProto = this._optionsUtil.getOptionsProto( + context, + this.knownOptions, + this.rawOptions + ); } _toProto(_: JsonProtoSerializer): ProtoStage { @@ -87,17 +87,21 @@ export abstract class Stage implements ProtoSerializable, UserData { * @beta */ export class AddFields extends Stage { - get _name(): string { return 'add_fields'; } - get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; + get _name(): string { + return 'add_fields'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private fields: Map, options: StageOptions) { + constructor(private fields: Map, options: StageOptions) { super(options); } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [toMapValue(serializer, this.fields)], + args: [toMapValue(serializer, this.fields)] }; } @@ -117,7 +121,7 @@ export class RemoveFields extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({}); - }; + } constructor(private fields: Field[], options: StageOptions) { super(options); @@ -130,7 +134,7 @@ export class RemoveFields extends Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: this.fields.map(f => f._toProto(serializer)), + args: this.fields.map(f => f._toProto(serializer)) }; } @@ -150,11 +154,13 @@ export class Aggregate extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({}); - }; + } - constructor(private groups: Map, - private accumulators: Map, - options: StageOptions) { + constructor( + private groups: Map, + private accumulators: Map, + options: StageOptions + ) { super(options); } @@ -168,7 +174,7 @@ export class Aggregate extends Stage { args: [ toMapValue(serializer, this.accumulators), toMapValue(serializer, this.groups) - ], + ] }; } @@ -189,9 +195,9 @@ export class Distinct extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({}); - }; + } - constructor(private groups: Map, options: StageOptions) { + constructor(private groups: Map, options: StageOptions) { super(options); } @@ -223,10 +229,10 @@ export class CollectionSource extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({ forceIndex: { - serverName: 'force_index', - }, + serverName: 'force_index' + } }); - }; + } private formattedCollectionPath: string; @@ -234,7 +240,9 @@ export class CollectionSource extends Stage { super(options); // prepend slash to collection string - this.formattedCollectionPath = collection.startsWith('/') ? collection : '/' + collection; + this.formattedCollectionPath = collection.startsWith('/') + ? collection + : '/' + collection; } /** @@ -244,7 +252,7 @@ export class CollectionSource extends Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [{referenceValue: this.formattedCollectionPath}], + args: [{ referenceValue: this.formattedCollectionPath }] }; } @@ -264,10 +272,10 @@ export class CollectionGroupSource extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({ forceIndex: { - serverName: 'force_index', - }, + serverName: 'force_index' + } }); - }; + } constructor(private collectionId: string, options: StageOptions) { super(options); @@ -280,7 +288,7 @@ export class CollectionGroupSource extends Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [{referenceValue: ''}, {stringValue: this.collectionId}], + args: [{ referenceValue: '' }, { stringValue: this.collectionId }] }; } @@ -289,13 +297,16 @@ export class CollectionGroupSource extends Stage { } } - /** * @beta */ export class DatabaseSource extends Stage { - get _name(): string { return 'database'; } - get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; + get _name(): string { + return 'database'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } /** * @internal @@ -322,13 +333,15 @@ export class DocumentsSource extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({}); - }; + } private formattedPaths: string[]; constructor(docPaths: string[], options: StageOptions) { super(options); - this.formattedPaths = docPaths.map(path => path.startsWith('/') ? path : '/' + path); + this.formattedPaths = docPaths.map(path => + path.startsWith('/') ? path : '/' + path + ); } /** @@ -339,7 +352,7 @@ export class DocumentsSource extends Stage { return { ...super._toProto(serializer), args: this.formattedPaths.map(p => { - return {referenceValue: p}; + return { referenceValue: p }; }) }; } @@ -353,11 +366,16 @@ export class DocumentsSource extends Stage { * @beta */ export class Where extends Stage { - get _name(): string { return 'where'; } - get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; + get _name(): string { + return 'where'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private condition: BooleanExpr, options: StageOptions) { - super(options);} + constructor(private condition: BooleanExpression, options: StageOptions) { + super(options); + } /** * @internal @@ -366,7 +384,7 @@ export class Where extends Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [this.condition._toProto(serializer)], + args: [this.condition._toProto(serializer)] }; } @@ -387,17 +405,20 @@ export class FindNearest extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({ limit: { - serverName: 'limit', + serverName: 'limit' }, distanceField: { - serverName: 'distance_field', - }, + serverName: 'distance_field' + } }); - }; + } - constructor(private vectorValue: Expr, - private field: Field, - private distanceMeasure: "euclidean" | "cosine" | "dot_product", options: StageOptions) { + constructor( + private vectorValue: Expression, + private field: Field, + private distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', + options: StageOptions + ) { super(options); } @@ -412,11 +433,11 @@ export class FindNearest extends Stage { this.field._toProto(serializer), this.vectorValue._toProto(serializer), toStringValue(this.distanceMeasure) - ], + ] }; } - _readUserData(context: ParseContext) : void { + _readUserData(context: ParseContext): void { super._readUserData(context); readUserDataHelper(this.vectorValue, context); readUserDataHelper(this.field, context); @@ -427,8 +448,12 @@ export class FindNearest extends Stage { * @beta */ export class Limit extends Stage { - get _name(): string { return 'limit'; } - get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; + get _name(): string { + return 'limit'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } constructor(private limit: number, options: StageOptions) { hardAssert( @@ -446,7 +471,7 @@ export class Limit extends Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [toNumber(serializer, this.limit)], + args: [toNumber(serializer, this.limit)] }; } } @@ -455,11 +480,16 @@ export class Limit extends Stage { * @beta */ export class Offset extends Stage { - get _name(): string { return 'offset'; } - get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; + get _name(): string { + return 'offset'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } constructor(private offset: number, options: StageOptions) { - super(options);} + super(options); + } /** * @internal @@ -468,7 +498,7 @@ export class Offset extends Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [toNumber(serializer, this.offset)], + args: [toNumber(serializer, this.offset)] }; } } @@ -477,11 +507,19 @@ export class Offset extends Stage { * @beta */ export class Select extends Stage { - get _name(): string { return 'select'; } - get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; + get _name(): string { + return 'select'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private selections: Map, options: StageOptions) { - super(options);} + constructor( + private selections: Map, + options: StageOptions + ) { + super(options); + } /** * @internal @@ -490,11 +528,11 @@ export class Select extends Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [toMapValue(serializer, this.selections)], + args: [toMapValue(serializer, this.selections)] }; } - _readUserData(context: ParseContext) : void { + _readUserData(context: ParseContext): void { super._readUserData(context); readUserDataHelper(this.selections, context); } @@ -510,7 +548,7 @@ export class Sort extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({}); - }; + } constructor(private orderings: Ordering[], options: StageOptions) { super(options); @@ -527,7 +565,7 @@ export class Sort extends Stage { }; } - _readUserData(context: ParseContext) : void { + _readUserData(context: ParseContext): void { super._readUserData(context); readUserDataHelper(this.orderings, context); } @@ -537,12 +575,20 @@ export class Sort extends Stage { * @beta */ export class Sample extends Stage { - get _name(): string { return 'sample'; } - get _optionsUtil(): OptionsUtil {return new OptionsUtil({});}; + get _name(): string { + return 'sample'; + } + get _optionsUtil(): OptionsUtil { + return new OptionsUtil({}); + } - constructor(private rate: number, - private mode: 'percent' | 'documents', options: StageOptions) { - super(options);} + constructor( + private rate: number, + private mode: 'percent' | 'documents', + options: StageOptions + ) { + super(options); + } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { @@ -551,7 +597,7 @@ export class Sample extends Stage { }; } - _readUserData(context: ParseContext) : void { + _readUserData(context: ParseContext): void { super._readUserData(context); } } @@ -566,7 +612,7 @@ export class Union extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({}); - }; + } constructor(private other: Pipeline, options: StageOptions) { super(options); @@ -575,11 +621,11 @@ export class Union extends Stage { _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [toPipelineValue(this.other._toProto(serializer))], + args: [toPipelineValue(this.other._toProto(serializer))] }; } - _readUserData(context: ParseContext) : void { + _readUserData(context: ParseContext): void { super._readUserData(context); } } @@ -595,25 +641,30 @@ export class Unnest extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({ indexField: { - serverName: 'index_field', - }, + serverName: 'index_field' + } }); - }; + } - constructor(private alias: string, - private expr: Expr, - options: StageOptions) { + constructor( + private alias: string, + private expr: Expression, + options: StageOptions + ) { super(options); } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [this.expr._toProto(serializer), field(this.alias)._toProto(serializer)], + args: [ + this.expr._toProto(serializer), + field(this.alias)._toProto(serializer) + ] }; } - _readUserData(context: ParseContext) : void { + _readUserData(context: ParseContext): void { super._readUserData(context); readUserDataHelper(this.expr, context); } @@ -631,20 +682,20 @@ export class Replace extends Stage { get _optionsUtil(): OptionsUtil { return new OptionsUtil({}); - }; + } - constructor(private map: Expr, options: StageOptions) { + constructor(private map: Expression, options: StageOptions) { super(options); } _toProto(serializer: JsonProtoSerializer): ProtoStage { return { ...super._toProto(serializer), - args: [this.map._toProto(serializer), toStringValue(Replace.MODE)], + args: [this.map._toProto(serializer), toStringValue(Replace.MODE)] }; } - _readUserData(context: ParseContext) : void { + _readUserData(context: ParseContext): void { super._readUserData(context); readUserDataHelper(this.map, context); } @@ -653,17 +704,17 @@ export class Replace extends Stage { /** * @beta */ -export class GenericStage extends Stage { +export class RawStage extends Stage { /** * @private * @internal */ constructor( private name: string, - private params: Array, + private params: Array, rawOptions: Record ) { - super({rawOptions}); + super({ rawOptions }); } /** @@ -678,7 +729,7 @@ export class GenericStage extends Stage { }; } - _readUserData(context: ParseContext) : void { + _readUserData(context: ParseContext): void { super._readUserData(context); readUserDataHelper(this.params, context); } @@ -692,7 +743,6 @@ export class GenericStage extends Stage { } } - /** * Helper to read user data across a number of different formats. * @param name Name of the calling function. Used for error messages when invalid user data is encountered. @@ -701,17 +751,12 @@ export class GenericStage extends Stage { * @private */ function readUserDataHelper< -T extends -| Map -| UserData[] -| UserData + T extends Map | UserData[] | UserData >(expressionMap: T, context: ParseContext): T { if (isUserData(expressionMap)) { expressionMap._readUserData(context); } else if (Array.isArray(expressionMap)) { - expressionMap.forEach(readableData => - readableData._readUserData(context) - ); + expressionMap.forEach(readableData => readableData._readUserData(context)); } else { expressionMap.forEach(expr => expr._readUserData(context)); } diff --git a/packages/firestore/src/lite-api/stage_options.ts b/packages/firestore/src/lite-api/stage_options.ts index 66c47fba1e9..e45c39e023e 100644 --- a/packages/firestore/src/lite-api/stage_options.ts +++ b/packages/firestore/src/lite-api/stage_options.ts @@ -1,17 +1,16 @@ -import {OneOf} from "../util/types"; +import { OneOf } from '../util/types'; import { AggregateWithAlias, - BooleanExpr, - Expr, + BooleanExpression, + Expression, Field, Ordering, Selectable -} from "./expressions"; -import {Pipeline} from "./pipeline"; -import {CollectionReference, DocumentReference} from "./reference"; -import {VectorValue} from "./vector_value"; - +} from './expressions'; +import { Pipeline } from './pipeline'; +import { CollectionReference, DocumentReference } from './reference'; +import { VectorValue } from './vector_value'; /** * Options defining how a Stage is evaluated. @@ -128,9 +127,9 @@ export type SelectStageOptions = StageOptions & { */ export type WhereStageOptions = StageOptions & { /** - * The {@link BooleanExpr} to apply as a filter for each input document to this stage. + * The {@link BooleanExpression} to apply as a filter for each input document to this stage. */ - condition: BooleanExpr; + condition: BooleanExpression; }; /** * Options defining how an OffsetStage is evaluated. See {@link Pipeline.offset}. @@ -215,10 +214,10 @@ export type FindNearestStageOptions = StageOptions & { */ export type ReplaceWithStageOptions = StageOptions & { /** - * The name of a field that contains a map or an {@link Expr} that + * The name of a field that contains a map or an {@link Expression} that * evaluates to a map. */ - map: Expr | string; + map: Expression | string; }; /** * Defines the options for evaluating a sample stage within a pipeline. diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 1f20d0474e0..f11781ac331 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -1473,7 +1473,6 @@ export function isProtoValueSerializable( ); } - export function toMapValue( serializer: JsonProtoSerializer, input: Map> diff --git a/packages/firestore/src/util/input_validation.ts b/packages/firestore/src/util/input_validation.ts index 4973cb68bca..f3b5dda6985 100644 --- a/packages/firestore/src/util/input_validation.ts +++ b/packages/firestore/src/util/input_validation.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import {DocumentData} from "../lite-api/reference"; +import { DocumentData } from '../lite-api/reference'; import { DocumentKey } from '../model/document_key'; import { ResourcePath } from '../model/path'; diff --git a/packages/firestore/src/util/pipeline_util.ts b/packages/firestore/src/util/pipeline_util.ts index 430eff8e51f..63b8fce1b48 100644 --- a/packages/firestore/src/util/pipeline_util.ts +++ b/packages/firestore/src/util/pipeline_util.ts @@ -1,29 +1,33 @@ -import { vector} from "../api"; +import { vector } from '../api'; import { _constant, - AggregateFunction, AggregateWithAlias, array, constant, - Expr, - ExprWithAlias, + AggregateFunction, + AggregateWithAlias, + array, + constant, + Expression, + AliasedExpression, field, - Field, map, + Field, + map, Selectable -} from "../lite-api/expressions"; -import {VectorValue} from "../lite-api/vector_value"; +} from '../lite-api/expressions'; +import { VectorValue } from '../lite-api/vector_value'; -import {isPlainObject} from "./input_validation"; -import {isFirestoreValue} from "./proto"; -import {isString} from "./types"; +import { isPlainObject } from './input_validation'; +import { isFirestoreValue } from './proto'; +import { isString } from './types'; export function selectablesToMap( selectables: Array -): Map { - const result = new Map(); +): Map { + const result = new Map(); for (const selectable of selectables) { if (typeof selectable === 'string') { result.set(selectable as string, field(selectable)); } else if (selectable instanceof Field) { result.set(selectable.alias, selectable.expr); - } else if (selectable instanceof ExprWithAlias) { + } else if (selectable instanceof AliasedExpression) { result.set(selectable.alias, selectable.expr); } } @@ -34,10 +38,7 @@ export function aliasedAggregateToMap( aliasedAggregatees: AggregateWithAlias[] ): Map { return aliasedAggregatees.reduce( - ( - map: Map, - selectable: AggregateWithAlias - ) => { + (map: Map, selectable: AggregateWithAlias) => { map.set(selectable.alias, selectable.aggregate as AggregateFunction); return map; }, @@ -54,9 +55,9 @@ export function aliasedAggregateToMap( * @param value */ export function vectorToExpr( - value: VectorValue | number[] | Expr -): Expr { - if (value instanceof Expr) { + value: VectorValue | number[] | Expression +): Expression { + if (value instanceof Expression) { return value; } else if (value instanceof VectorValue) { const result = constant(value); @@ -79,7 +80,7 @@ export function vectorToExpr( * @internal * @param value */ -export function fieldOrExpression(value: unknown): Expr { +export function fieldOrExpression(value: unknown): Expression { if (isString(value)) { const result = field(value); return result; @@ -95,12 +96,12 @@ export function fieldOrExpression(value: unknown): Expr { * @internal * @param value */ -export function valueToDefaultExpr(value: unknown): Expr { - let result: Expr | undefined; +export function valueToDefaultExpr(value: unknown): Expression { + let result: Expression | undefined; if (isFirestoreValue(value)) { return constant(value); } - if (value instanceof Expr) { + if (value instanceof Expression) { return value; } else if (isPlainObject(value)) { result = map(value as Record); diff --git a/packages/firestore/src/util/types.ts b/packages/firestore/src/util/types.ts index 5cd3fab71ca..e1b2bfb86c2 100644 --- a/packages/firestore/src/util/types.ts +++ b/packages/firestore/src/util/types.ts @@ -15,14 +15,18 @@ * limitations under the License. */ -import {CollectionReference} from "../api"; +import { CollectionReference } from '../api'; import { AggregateFunction, - AggregateWithAlias, BooleanExpr, Expr, field, Field, + AggregateWithAlias, + BooleanExpression, + Expression, + field, + Field, Ordering, Selectable -} from "../lite-api/expressions"; -import {Pipeline as LitePipeline} from "../lite-api/pipeline"; +} from '../lite-api/expressions'; +import { Pipeline as LitePipeline } from '../lite-api/pipeline'; /** Sentinel value that sorts before any Mutation Batch ID. */ export const BATCHID_UNKNOWN = -1; @@ -90,20 +94,17 @@ export interface DocumentLike { */ export type OneOf = { [K in keyof T]: Pick & { - [P in Exclude]?: undefined; -}; + [P in Exclude]?: undefined; + }; }[keyof T]; -export function isSelectable( - val: unknown -): val is Selectable { +export function isSelectable(val: unknown): val is Selectable { const candidate = val as Selectable; return ( candidate.selectable && isString(candidate.alias) && isExpr(candidate.expr) ); } - export function isOrdering(val: unknown): val is Ordering { const candidate = val as Ordering; return ( @@ -113,9 +114,7 @@ export function isOrdering(val: unknown): val is Ordering { ); } -export function isAliasedAggregate( - val: unknown -): val is AggregateWithAlias { +export function isAliasedAggregate(val: unknown): val is AggregateWithAlias { const candidate = val as AggregateWithAlias; return ( isString(candidate.alias) && @@ -123,14 +122,12 @@ export function isAliasedAggregate( ); } -export function isExpr(val: unknown): val is Expr { - return val instanceof Expr; +export function isExpr(val: unknown): val is Expression { + return val instanceof Expression; } -export function isBooleanExpr( - val: unknown -): val is BooleanExpr { - return val instanceof BooleanExpr; +export function isBooleanExpr(val: unknown): val is BooleanExpression { + return val instanceof BooleanExpression; } export function isField(val: unknown): val is Field { diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index e91c828be44..fe1004ac81f 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -20,12 +20,12 @@ import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { - AggregateFunction, arrayGet, + AggregateFunction, + arrayGet, ascending, - BooleanExpr, + BooleanExpression, byteLength, - constantVector, - FunctionExpr, + FunctionExpression, timestampAdd, timestampToUnixMicros, timestampToUnixMillis, @@ -33,7 +33,19 @@ import { toLower, unixMicrosToTimestamp, unixMillisToTimestamp, - vectorLength + vectorLength, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + length } from '../../../src/lite-api/expressions'; import { PipelineSnapshot } from '../../../src/lite-api/pipeline-result'; import { addEqualityMatcher } from '../../util/equality_matcher'; @@ -55,15 +67,15 @@ import { documentId as documentIdFieldPath, writeBatch, addDoc, + DocumentReference, + deleteDoc } from '../util/firebase_export'; -import { apiDescribe, withTestCollection, itIf } from '../util/helpers'; +import { apiDescribe, withTestCollection } from '../util/helpers'; import { array, mod, pipelineResultEqual, sum, - replaceFirst, - replaceAll, descending, isNan, map, @@ -75,47 +87,39 @@ import { arrayContains, arrayContainsAny, count, - avg, + average, cosineDistance, not, countAll, dotProduct, endsWith, - eq, + equal, reverse, toUpper, euclideanDistance, - gt, + greaterThan, like, - lt, - strContains, + lessThan, + stringContains, divide, - lte, + lessThanOrEqual, arrayLength, - arrayConcat, mapGet, - neq, + notEqual, or, regexContains, regexMatch, startsWith, - strConcat, + stringConcat, subtract, - cond, - eqAny, + conditional, + equalAny, logicalMaximum, - notEqAny, + notEqualAny, multiply, countIf, - bitAnd, - bitOr, - bitXor, - bitNot, exists, - bitLeftShift, charLength, - bitRightShift, - rand, minimum, maximum, isError, @@ -125,23 +129,23 @@ import { isNull, isNotNull, isNotNan, - timestampSub, + timestampSubtract, mapRemove, mapMerge, documentId, - substr, + substring, logicalMinimum, xor, field, constant, - _internalPipelineToExecutePipelineRequestProto, FindNearestStageOptions, + _internalPipelineToExecutePipelineRequestProto, + FindNearestStageOptions } from '../util/pipeline_export'; use(chaiAsPromised); setLogLevel('debug'); -const testUnsupportedFeatures: boolean | 'only' = false; const timestampDeltaMS = 1000; apiDescribe.only('Pipelines', persistence => { @@ -332,6 +336,23 @@ apiDescribe.only('Pipelines', persistence => { await withTestCollectionPromise; }); + describe('console support', () => { + it('supports internal serialization to proto', async () => { + const pipeline = firestore + .pipeline() + .collection('books') + .where(equal('awards.hugo', true)) + .select( + 'title', + field('nestedField.level.1'), + mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + ); + + const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); + expect(proto).not.to.be.null; + }); + }); + describe('pipeline results', () => { it('empty snapshot as expected', async () => { const snapshot = await execute( @@ -347,7 +368,6 @@ apiDescribe.only('Pipelines', persistence => { .sort(ascending('__name__')); const snapshot = await execute(ppl); expect(snapshot.results.length).to.equal(10); - expect(snapshot.pipeline).to.equal(ppl); expectResults( snapshot, 'book1', @@ -447,7 +467,7 @@ apiDescribe.only('Pipelines', persistence => { const pipeline = firestore .pipeline() .collection(randomCol.path) - .aggregate(avg('rating').as('avgRating')); + .aggregate(average('rating').as('avgRating')); const snapshot = await execute(pipeline); const end = new Date().valueOf(); @@ -465,7 +485,7 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .aggregate({ - accumulators: [avg('rating').as('avgRating')], + accumulators: [average('rating').as('avgRating')], groups: ['genre'] }); @@ -523,31 +543,26 @@ apiDescribe.only('Pipelines', persistence => { await terminate(db2); }); - // Subcollections not currently supported in DBE - itIf(testUnsupportedFeatures)( - 'supports collection group as source', - async () => { - const randomSubCollectionId = Math.random().toString(16).slice(2); - const doc1 = await addDoc( - collection(randomCol, 'book1', randomSubCollectionId), - { order: 1 } - ); - const doc2 = await addDoc( - collection(randomCol, 'book2', randomSubCollectionId), - { order: 2 } - ); - const snapshot = await execute( - firestore - .pipeline() - .collectionGroup(randomSubCollectionId) - .sort(ascending('order')) - ); - expectResults(snapshot, doc1.id, doc2.id); - } - ); + it('supports collection group as source', async () => { + const randomSubCollectionId = Math.random().toString(16).slice(2); + const doc1 = await addDoc( + collection(randomCol, 'book1', randomSubCollectionId), + { order: 1 } + ); + const doc2 = await addDoc( + collection(randomCol, 'book2', randomSubCollectionId), + { order: 2 } + ); + const snapshot = await execute( + firestore + .pipeline() + .collectionGroup(randomSubCollectionId) + .sort(ascending('order')) + ); + expectResults(snapshot, doc1.id, doc2.id); + }); - // subcollections not currently supported in dbe - itIf(testUnsupportedFeatures)('supports database as source', async () => { + it('supports database as source', async () => { const randomId = Math.random().toString(16).slice(2); const doc1 = await addDoc(collection(randomCol, 'book1', 'sub'), { order: 1, @@ -561,7 +576,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .database() - .where(eq('randomId', randomId)) + .where(equal('randomId', randomId)) .sort(ascending('order')) ); expectResults(snapshot, doc1.id, doc2.id); @@ -584,8 +599,7 @@ apiDescribe.only('Pipelines', persistence => { Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) ).as('bytes'), constant(doc(firestore, 'foo', 'bar')).as('documentReference'), - constantVector(vector([1, 2, 3])).as('vectorValue'), - constantVector([1, 2, 3]).as('vectorValue2'), + constant(vector([1, 2, 3])).as('vectorValue'), map({ 'number': 1, 'string': 'a string', @@ -642,7 +656,6 @@ apiDescribe.only('Pipelines', persistence => { 'bytes': Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])), 'documentReference': doc(firestore, 'foo', 'bar'), 'vectorValue': vector([1, 2, 3]), - 'vectorValue2': vector([1, 2, 3]), 'map': { 'number': 1, 'string': 'a string', @@ -748,7 +761,7 @@ apiDescribe.only('Pipelines', persistence => { ) .where( and( - eq('metadataArray', [ + equal('metadataArray', [ 1, 2, field('genre'), @@ -758,7 +771,7 @@ apiDescribe.only('Pipelines', persistence => { published: field('published') } ]), - eq('metadata', { + equal('metadata', { genre: field('genre'), rating: multiply('rating', 10), nestedArray: [field('title')], @@ -817,10 +830,10 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('genre', 'Science Fiction')) + .where(equal('genre', 'Science Fiction')) .aggregate( countAll().as('count'), - avg('rating').as('avgRating'), + average('rating').as('avgRating'), maximum('rating').as('maxRating'), sum('rating').as('sumRating') ) @@ -834,29 +847,33 @@ apiDescribe.only('Pipelines', persistence => { }); it('supports aggregate options', async () => { - let snapshot = await execute(firestore - .pipeline() - .collection(randomCol.path) - .aggregate({ - accumulators: [countAll().as('count')], - })); - expectResults(snapshot, {count: 10}); + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [countAll().as('count')] + }) + ); + expectResults(snapshot, { count: 10 }); - snapshot = await execute(firestore - .pipeline() - .collection(randomCol.path) - .where(eq('genre', 'Science Fiction')) - .aggregate( - countAll().as('count'), - avg('rating').as('avgRating'), - maximum('rating').as('maxRating'), - sum('rating').as('sumRating') - )); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + average('rating').as('avgRating'), + maximum('rating').as('maxRating'), + sum('rating').as('sumRating') + ) + ); expectResults(snapshot, { count: 2, avgRating: 4.4, maxRating: 4.6, - sumRating: 8.8, + sumRating: 8.8 }); }); @@ -866,7 +883,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(lt('published', 1900)) + .where(lessThan('published', 1900)) .aggregate({ accumulators: [], groups: ['genre'] @@ -880,12 +897,12 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(lt(field('published'), 1984)) + .where(lessThan(field('published'), 1984)) .aggregate({ - accumulators: [avg('rating').as('avgRating')], + accumulators: [average('rating').as('avgRating')], groups: ['genre'] }) - .where(gt('avgRating', 4.3)) + .where(greaterThan('avgRating', 4.3)) .sort(field('avgRating').descending()) ); expectResults( @@ -921,7 +938,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .aggregate(countIf(field('rating').gt(4.3)).as('count')) + .aggregate(countIf(field('rating').greaterThan(4.3)).as('count')) ); const expectedResults = { count: 3 @@ -932,10 +949,20 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .aggregate(field('rating').gt(4.3).countIf().as('count')) + .aggregate(field('rating').greaterThan(4.3).countIf().as('count')) ); expectResults(snapshot, expectedResults); }); + + it('returns countDistinct accumulation', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countDistinct('genre').as('distinctGenres')) + ); + expectResults(snapshot, { distinctGenres: 8 }); + }); }); describe('distinct stage', () => { @@ -967,8 +994,13 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .distinct({groups: ['genre', 'author']}) - .sort(field('genre').ascending(), field('author').ascending()) + .distinct('genre', 'author') + .sort({ + orderings: [ + field('genre').ascending(), + field('author').ascending() + ] + }) ); expectResults( snapshot, @@ -1017,19 +1049,21 @@ apiDescribe.only('Pipelines', persistence => { }); it('supports options', async () => { - const snapshot = await execute(firestore - .pipeline() - .collection(randomCol.path) - .select({selections: ['title', field('author').as('auth0r')]}) - .sort(field('auth0r').ascending()) - .limit(2)); + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select({ selections: ['title', field('author').as('auth0r')] }) + .sort(field('auth0r').ascending()) + .limit(2) + ); expectResults( snapshot, { title: "The Hitchhiker's Guide to the Galaxy", - auth0r: 'Douglas Adams', + auth0r: 'Douglas Adams' }, - {title: 'The Great Gatsby', auth0r: 'F. Scott Fitzgerald'} + { title: 'The Great Gatsby', auth0r: 'F. Scott Fitzgerald' } ); }); }); @@ -1183,14 +1217,97 @@ apiDescribe.only('Pipelines', persistence => { }); it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'genre') + .sort(field('author').ascending()) + .removeFields({ + fields: [field('author'), 'genre'] + }) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, + { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, + { title: 'Pride and Prejudice' }, + { + title: "The Handmaid's Tale" + } + ); + }); + }); + + describe('findNearest stage', () => { + it('can find nearest', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select('title', 'author') .sort(field('author').ascending()) - .removeFields({fields: [field('author')] - }) + .removeFields(field('author')) + .sort(field('author').ascending()) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, + { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, + { title: 'Pride and Prejudice' }, + { + title: "The Handmaid's Tale" + } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author', 'genre') + .sort(field('author').ascending()) + .removeFields({ + fields: [field('author'), 'genre'] + }) .sort(field('author').ascending()) ); expectResults( @@ -1231,13 +1348,14 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .where( and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) ) ) ); expectResults(snapshot, 'book10', 'book4'); }); + it('where with and (3 conditions)', async () => { const snapshot = await execute( firestore @@ -1245,14 +1363,15 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .where( and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), - lt('published', 1965) + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lessThan('published', 1965) ) ) ); expectResults(snapshot, 'book4'); }); + it('where with or', async () => { const snapshot = await execute( firestore @@ -1260,9 +1379,9 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .where( or( - eq('genre', 'Romance'), - eq('genre', 'Dystopian'), - eq('genre', 'Fantasy') + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy') ) ) .sort(ascending('title')) @@ -1284,10 +1403,10 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .where( xor( - eq('genre', 'Romance'), - eq('genre', 'Dystopian'), - eq('genre', 'Fantasy'), - eq('published', 1949) + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy'), + equal('published', 1949) ) ) .select('title') @@ -1301,15 +1420,17 @@ apiDescribe.only('Pipelines', persistence => { }); it('supports options', async () => { - const snapshot = await execute(firestore - .pipeline() - .collection(randomCol.path) - .where({ - condition: and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) - ), - })); + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where({ + condition: and( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ) + }) + ); expectResults(snapshot, 'book10', 'book4'); }); }); @@ -1334,25 +1455,27 @@ apiDescribe.only('Pipelines', persistence => { }); it('sort, offset, and limit stages support options', async () => { - const snapshot = await execute(firestore - .pipeline() - .collection(randomCol.path) - .sort({ - orderings: [field('author').ascending()], - }) - .offset({offset: 5}) - .limit({limit: 3}) - .select('title', 'author')); + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort({ + orderings: [field('author').ascending()] + }) + .offset({ offset: 5 }) + .limit({ limit: 3 }) + .select('title', 'author') + ); expectResults( snapshot, - {title: '1984', author: 'George Orwell'}, - {title: 'To Kill a Mockingbird', author: 'Harper Lee'}, - {title: 'The Lord of the Rings', author: 'J.R.R. Tolkien'} + { title: '1984', author: 'George Orwell' }, + { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, + { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } ); }); }); - describe('generic stage', () => { + describe('raw stage', () => { it('can select fields', async () => { const snapshot = await execute( firestore @@ -1362,7 +1485,7 @@ apiDescribe.only('Pipelines', persistence => { { title: field('title'), metadata: { - 'author': field('author') + author: field('author') } } ]) @@ -1387,7 +1510,7 @@ apiDescribe.only('Pipelines', persistence => { .select('title', 'author') .rawStage('add_fields', [ { - display: strConcat('title', ' - ', field('author')) + display: stringConcat('title', ' - ', field('author')) } ]) ); @@ -1404,7 +1527,7 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .rawStage('where', [field('author').eq('Douglas Adams')]) + .rawStage('where', [field('author').equal('Douglas Adams')]) ); expectResults(snapshot, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1440,7 +1563,7 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .select('title', 'author', 'rating') .rawStage('aggregate', [ - { averageRating: field('rating').avg() }, + { averageRating: field('rating').average() }, {} ]) ); @@ -1485,32 +1608,33 @@ apiDescribe.only('Pipelines', persistence => { }); it('can perform FindNearest query', async () => { - const snapshot = await execute(firestore - .pipeline() - .collection(randomCol) - .rawStage( - 'find_nearest', - [ - field('embedding'), - vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), - 'euclidean', - ], - { - 'distance_field': field('computedDistance'), - limit: 2, - } - ) - .select('title', 'computedDistance')); - + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .rawStage( + 'find_nearest', + [ + field('embedding'), + vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), + 'euclidean' + ], + { + 'distance_field': field('computedDistance'), + limit: 2 + } + ) + .select('title', 'computedDistance') + ); expectResults( snapshot, { title: "The Hitchhiker's Guide to the Galaxy", - computedDistance: 1, + computedDistance: 1 }, { title: 'One Hundred Years of Solitude', - computedDistance: 12.041594578792296, + computedDistance: 12.041594578792296 } ); }); @@ -1522,7 +1646,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .replaceWith('awards') ); expectResults(snapshot, { @@ -1537,7 +1661,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .replaceWith( map({ foo: 'bar', @@ -1554,15 +1678,17 @@ apiDescribe.only('Pipelines', persistence => { }); it('supports options', async () => { - const snapshot = await execute(firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) - .replaceWith({map: 'awards'})); + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith({ map: 'awards' }) + ); expectResults(snapshot, { hugo: true, nebula: false, - others: {unknown: {year: 1980}}, + others: { unknown: { year: 1980 } } }); }); }); @@ -1587,7 +1713,7 @@ apiDescribe.only('Pipelines', persistence => { it('run pipeline with sample limit of {percentage: 0.6}', async () => { let avgSize = 0; - const numIterations = 20; + const numIterations = 30; for (let i = 0; i < numIterations; i++) { const snapshot = await execute( firestore @@ -1638,11 +1764,13 @@ apiDescribe.only('Pipelines', persistence => { }); it('supports options', async () => { - const snapshot = await execute(firestore - .pipeline() - .collection(randomCol.path) - .union({other: firestore.pipeline().collection(randomCol.path)}) - .sort(field(documentIdFieldPath()).ascending())); + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .union({ other: firestore.pipeline().collection(randomCol.path) }) + .sort(field(documentIdFieldPath()).ascending()) + ); expectResults( snapshot, 'book1', @@ -1675,7 +1803,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .unnest(field('tags').as('tag')) .select( 'title', @@ -1740,26 +1868,25 @@ apiDescribe.only('Pipelines', persistence => { }); it('unnest with index field', async () => { - const snapshot = await execute(firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) - .unnest({ - selectable: field('tags').as('tag'), - indexField: 'tagsIndex' - }) - .select( - 'title', - 'author', - 'genre', - 'published', - 'rating', - 'tags', - 'tag', - 'awards', - 'nestedField', - 'tagsIndex' - )); + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(field('tags').as('tag'), 'tagsIndex') + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField', + 'tagsIndex' + ) + ); expectResults( snapshot, { @@ -1773,10 +1900,10 @@ apiDescribe.only('Pipelines', persistence => { awards: { hugo: true, nebula: false, - others: {unknown: {year: 1980}}, + others: { unknown: { year: 1980 } } }, - nestedField: {'level.1': {'level.2': true}}, - tagsIndex: 0, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 0 }, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1789,10 +1916,10 @@ apiDescribe.only('Pipelines', persistence => { awards: { hugo: true, nebula: false, - others: {unknown: {year: 1980}}, + others: { unknown: { year: 1980 } } }, - nestedField: {'level.1': {'level.2': true}}, - tagsIndex: 1, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 1 }, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1805,10 +1932,10 @@ apiDescribe.only('Pipelines', persistence => { awards: { hugo: true, nebula: false, - others: {unknown: {year: 1980}}, + others: { unknown: { year: 1980 } } }, - nestedField: {'level.1': {'level.2': true}}, - tagsIndex: 2, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 2 } ); }); @@ -1818,7 +1945,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .unnest(array([1, 2, 3]).as('copy')) .select( 'title', @@ -1881,25 +2008,101 @@ apiDescribe.only('Pipelines', persistence => { } ); }); - }); - describe('findNearest stage', () => { - it('run pipeline with findNearest', async () => { - const measures: Array = [ - 'euclidean', - 'dot_product', - 'cosine' - ]; - for (const measure of measures) { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol) - .findNearest({ - field: 'embedding', - vectorValue: vector([10, 1, 3, 1, 2, 1, 1, 1, 1, 1]), - limit: 3, - distanceMeasure: measure + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest({ + selectable: field('tags').as('tag'), + indexField: 'tagsIndex' + }) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField', + 'tagsIndex' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'comedy', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 0 + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'space', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 1 + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'adventure', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 2 + } + ); + }); + }); + + describe('findNearest stage', () => { + it('run pipeline with findNearest', async () => { + const measures: Array = [ + 'euclidean', + 'dot_product', + 'cosine' + ]; + for (const measure of measures) { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .findNearest({ + field: 'embedding', + vectorValue: vector([10, 1, 3, 1, 2, 1, 1, 1, 1, 1]), + limit: 3, + distanceMeasure: measure }) .select('title') ); @@ -1955,7 +2158,7 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .rawStage('select', [ // incorrect parameter type - field('title'), + field('title') ]); await execute(myPipeline); @@ -1967,9 +2170,7 @@ apiDescribe.only('Pipelines', persistence => { expect(err['code']).to.equal('invalid-argument'); expect(typeof err['message']).to.equal('string'); - expect(err['message']).to.match( - /^3 INVALID_ARGUMENT: .*$/ - ); + expect(err['message']).to.match(/^3 INVALID_ARGUMENT: .*$/); } }); }); @@ -2019,15 +2220,15 @@ apiDescribe.only('Pipelines', persistence => { ); }); - it('cond works', async () => { + it('conditional works', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select( 'title', - cond( - lt(field('published'), 1960), + conditional( + lessThan(field('published'), 1960), constant(1960), field('published') ).as('published-safe') @@ -2043,12 +2244,12 @@ apiDescribe.only('Pipelines', persistence => { ); }); - it('eqAny works', async () => { + it('equalAny works', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where(eqAny('published', [1979, 1999, 1967])) + .where(equalAny('published', [1979, 1999, 1967])) .sort(descending('title')) .select('title') ); @@ -2059,13 +2260,13 @@ apiDescribe.only('Pipelines', persistence => { ); }); - it('notEqAny works', async () => { + it('notEqualAny works', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .where( - notEqAny( + notEqualAny( 'published', [1965, 1925, 1949, 1960, 1866, 1985, 1954, 1967, 1979] ) @@ -2121,7 +2322,7 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select(arrayLength('tags').as('tagsCount')) - .where(eq('tagsCount', 3)) + .where(equal('tagsCount', 3)) ); expect(snapshot.results.length).to.equal(10); }); @@ -2133,7 +2334,7 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .sort(ascending('author')) .select( - field('author').strConcat(' - ', field('title')).as('bookInfo') + field('author').stringConcat(' - ', field('title')).as('bookInfo') ) .limit(1) ); @@ -2181,7 +2382,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(strContains('title', "'s")) + .where(stringContains('title', "'s")) .select('title') .sort(field('title').ascending()) ); @@ -2198,7 +2399,7 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select(charLength('title').as('titleLength'), field('title')) - .where(gt('titleLength', 20)) + .where(greaterThan('titleLength', 20)) .sort(field('title').ascending()) ); @@ -2262,14 +2463,14 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', 'To Kill a Mockingbird')) + .where(equal('title', 'To Kill a Mockingbird')) .select( add(field('rating'), 1).as('ratingPlusOne'), subtract(field('published'), 1900).as('yearsSince1900'), field('rating').multiply(10).as('ratingTimesTen'), divide('rating', 2).as('ratingDividedByTwo'), - multiply('rating', 10).as('ratingTimes20'), - add('rating', 1).as('ratingPlus3'), + multiply('rating', 20).as('ratingTimes20'), + add('rating', 3).as('ratingPlus3'), mod('rating', 2).as('ratingMod2') ) .limit(1) @@ -2292,9 +2493,9 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .where( and( - gt('rating', 4.2), - lte(field('rating'), 4.5), - neq('genre', 'Science Fiction') + greaterThan('rating', 4.2), + lessThanOrEqual(field('rating'), 4.5), + notEqual('genre', 'Science Fiction') ) ) .select('rating', 'title') @@ -2318,8 +2519,11 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .where( or( - and(gt('rating', 4.5), eq('genre', 'Science Fiction')), - lt('published', 1900) + and( + greaterThan('rating', 4.5), + equal('genre', 'Science Fiction') + ), + lessThan('published', 1900) ) ) .select('title') @@ -2343,8 +2547,8 @@ apiDescribe.only('Pipelines', persistence => { .select( isNull('rating').as('ratingIsNull'), isNan('rating').as('ratingIsNaN'), - isError(arrayGet('title', 0)).as('isError'), - ifError(arrayGet('title', 0), constant('was error')).as( + isError(divide(constant(1), constant(0))).as('isError'), + ifError(divide(constant(1), constant(0)), constant('was error')).as( 'ifError' ), isAbsent('foo').as('isAbsent'), @@ -2375,8 +2579,8 @@ apiDescribe.only('Pipelines', persistence => { .select( field('rating').isNull().as('ratingIsNull'), field('rating').isNan().as('ratingIsNaN'), - arrayGet('title', 0).isError().as('isError'), - arrayGet('title', 0) + divide(constant(1), constant(0)).isError().as('isError'), + divide(constant(1), constant(0)) .ifError(constant('was error')) .as('ifError'), field('foo').isAbsent().as('isAbsent'), @@ -2406,7 +2610,7 @@ apiDescribe.only('Pipelines', persistence => { field('awards').mapGet('others').as('others'), field('title') ) - .where(eq('hugoAward', true)) + .where(equal('hugoAward', true)) ); expectResults( snapshot, @@ -2415,25 +2619,25 @@ apiDescribe.only('Pipelines', persistence => { title: "The Hitchhiker's Guide to the Galaxy", others: { unknown: { year: 1980 } } }, - { hugoAward: true, title: 'Dune', others: null } + { hugoAward: true, title: 'Dune' } ); }); it('testDistanceFunctions', async () => { - const sourceVector = [0.1, 0.1]; - const targetVector = [0.5, 0.8]; + const sourceVector = vector([0.1, 0.1]); + const targetVector = vector([0.5, 0.8]); let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select( - cosineDistance(constantVector(sourceVector), targetVector).as( + cosineDistance(constant(sourceVector), targetVector).as( 'cosineDistance' ), - dotProduct(constantVector(sourceVector), targetVector).as( + dotProduct(constant(sourceVector), targetVector).as( 'dotProductDistance' ), - euclideanDistance(constantVector(sourceVector), targetVector).as( + euclideanDistance(constant(sourceVector), targetVector).as( 'euclideanDistance' ) ) @@ -2451,13 +2655,13 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .select( - constantVector(sourceVector) + constant(sourceVector) .cosineDistance(targetVector) .as('cosineDistance'), - constantVector(sourceVector) + constant(sourceVector) .dotProduct(targetVector) .as('dotProductDistance'), - constantVector(sourceVector) + constant(sourceVector) .euclideanDistance(targetVector) .as('euclideanDistance') ) @@ -2477,7 +2681,7 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .limit(1) - .select(vectorLength(constantVector([1, 2, 3])).as('vectorLength')) + .select(vectorLength(constant(vector([1, 2, 3]))).as('vectorLength')) ); expectResults(snapshot, { vectorLength: 3 @@ -2489,7 +2693,7 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('awards.hugo', true)) + .where(equal('awards.hugo', true)) .sort(descending('title')) .select('title', 'awards.hugo') ); @@ -2508,23 +2712,31 @@ apiDescribe.only('Pipelines', persistence => { firestore .pipeline() .collection(randomCol.path) - .where(eq('awards.hugo', true)) + .limit(1) + .replaceWith( + map({ + title: 'foo', + nested: { + level: { + '1': 'bar' + }, + 'level.1': { + 'level.2': 'baz' + } + } + }) + ) .select( 'title', - field('nestedField.level.1'), - mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + field('nested.level.1'), + mapGet('nested', 'level.1').mapGet('level.2').as('nested') ) - .sort(descending('title')) - ); - expectResults( - snapshot, - { - title: "The Hitchhiker's Guide to the Galaxy", - 'nestedField.level.`1`': null, - nested: true - }, - { title: 'Dune', 'nestedField.level.`1`': null, nested: null } ); + expectResults(snapshot, { + title: 'foo', + 'nested.level.`1`': 'bar', + nested: 'baz' + }); }); describe('rawFunction', () => { @@ -2536,7 +2748,7 @@ apiDescribe.only('Pipelines', persistence => { .sort(descending('rating')) .limit(1) .select( - new FunctionExpr('add', [field('rating'), constant(1)]).as( + new FunctionExpression('add', [field('rating'), constant(1)]).as( 'rating' ) ) @@ -2552,9 +2764,9 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .where( - new BooleanExpr('and', [ - field('rating').gt(0), - field('title').charLength().lt(5), + new BooleanExpression('and', [ + field('rating').greaterThan(0), + field('title').charLength().lessThan(5), field('tags').arrayContains('propaganda') ]) ) @@ -2565,13 +2777,14 @@ apiDescribe.only('Pipelines', persistence => { }); }); - it('array contains any', async () => { + // TODO re-enabled on fix of b/446938511 + it.skip('array contains any', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .where( - new BooleanExpr('array_contains_any', [ + new BooleanExpression('array_contains_any', [ field('tags'), array(['politics']) ]) @@ -2589,9 +2802,9 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .aggregate( - new AggregateFunction('count_if', [field('rating').gte(4.5)]).as( - 'countOfBest' - ) + new AggregateFunction('count_if', [ + field('rating').greaterThanOrEqual(4.5) + ]).as('countOfBest') ) ); expectResults(snapshot, { @@ -2605,7 +2818,9 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .sort( - new FunctionExpr('char_length', [field('title')]).ascending(), + new FunctionExpression('char_length', [ + field('title') + ]).ascending(), descending('__name__') ) .limit(3) @@ -2626,45 +2841,6 @@ apiDescribe.only('Pipelines', persistence => { }); }); - itIf(testUnsupportedFeatures)('testReplaceFirst', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', 'The Lord of the Rings')) - .limit(1) - .select(replaceFirst('title', 'o', '0').as('newName')) - ); - expectResults(snapshot, { newName: 'The L0rd of the Rings' }); - }); - - itIf(testUnsupportedFeatures)('testReplaceAll', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', 'The Lord of the Rings')) - .limit(1) - .select(replaceAll('title', 'o', '0').as('newName')) - ); - expectResults(snapshot, { newName: 'The L0rd 0f the Rings' }); - }); - - it('supports Rand', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(10) - .select(rand().as('result')) - ); - expect(snapshot.results.length).to.equal(10); - snapshot.results.forEach(d => { - expect(d.get('result')).to.be.lt(1); - expect(d.get('result')).to.be.gte(0); - }); - }); - it('supports array', async () => { const snapshot = await execute( firestore @@ -2697,7 +2873,7 @@ apiDescribe.only('Pipelines', persistence => { }); }); - it('supports arrayOffset', async () => { + it('supports arrayGet', async () => { let snapshot = await execute( firestore .pipeline() @@ -2877,12 +3053,16 @@ apiDescribe.only('Pipelines', persistence => { timestampAdd('timestamp', 'second', 10).as('plus10seconds'), timestampAdd('timestamp', 'microsecond', 10).as('plus10micros'), timestampAdd('timestamp', 'millisecond', 10).as('plus10millis'), - timestampSub('timestamp', 'day', 10).as('minus10days'), - timestampSub('timestamp', 'hour', 10).as('minus10hours'), - timestampSub('timestamp', 'minute', 10).as('minus10minutes'), - timestampSub('timestamp', 'second', 10).as('minus10seconds'), - timestampSub('timestamp', 'microsecond', 10).as('minus10micros'), - timestampSub('timestamp', 'millisecond', 10).as('minus10millis') + timestampSubtract('timestamp', 'day', 10).as('minus10days'), + timestampSubtract('timestamp', 'hour', 10).as('minus10hours'), + timestampSubtract('timestamp', 'minute', 10).as('minus10minutes'), + timestampSubtract('timestamp', 'second', 10).as('minus10seconds'), + timestampSubtract('timestamp', 'microsecond', 10).as( + 'minus10micros' + ), + timestampSubtract('timestamp', 'millisecond', 10).as( + 'minus10millis' + ) ) ); expectResults(snapshot, { @@ -2899,7 +3079,7 @@ apiDescribe.only('Pipelines', persistence => { minus10micros: new Timestamp(1741380234, 999990000), minus10millis: new Timestamp(1741380234, 990000000) }); - }); + }).timeout(10000); it('supports byteLength', async () => { const snapshot = await execute( @@ -2927,7 +3107,7 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol) .limit(1) .select(constant(true).as('trueField')) - .select('trueField', not(eq('trueField', true)).as('falseField')) + .select('trueField', not(equal('trueField', true)).as('falseField')) ); expectResults(snapshot, { @@ -2935,336 +3115,622 @@ apiDescribe.only('Pipelines', persistence => { falseField: false }); }); - }); - describe('not yet implemented in backend', () => { - itIf(testUnsupportedFeatures)('supports Bit_and', async () => { + it('can reverse an array', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(bitAnd(constant(5), 12).as('result')) + .select(field('tags').arrayReverse().as('reversedTags')) ); expectResults(snapshot, { - result: 4 + reversedTags: ['adventure', 'space', 'comedy'] }); }); - itIf(testUnsupportedFeatures)('supports Bit_and', async () => { + it('can reverse an array with the top-level function', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(constant(5).bitAnd(12).as('result')) + .select(reverse('tags').as('reversedTags')) ); expectResults(snapshot, { - result: 4 + reversedTags: ['adventure', 'space', 'comedy'] }); }); - itIf(testUnsupportedFeatures)('supports Bit_or', async () => { - let snapshot = await execute( + it('can compute the ceiling of a numeric value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(bitOr(constant(5), 12).as('result')) + .select(field('rating').ceil().as('ceilingRating')) ); expectResults(snapshot, { - result: 13 + ceilingRating: 5 }); - snapshot = await execute( + }); + + it('can compute the ceiling of a numeric value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(constant(5).bitOr(12).as('result')) + .select(ceil('rating').as('ceilingRating')) ); expectResults(snapshot, { - result: 13 + ceilingRating: 5 }); }); - itIf(testUnsupportedFeatures)('supports Bit_xor', async () => { - let snapshot = await execute( + it('can compute the floor of a numeric value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(bitXor(constant(5), 12).as('result')) + .select(field('rating').floor().as('floorRating')) ); expectResults(snapshot, { - result: 9 + floorRating: 4 }); - snapshot = await execute( + }); + + it('can compute the floor of a numeric value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(constant(5).bitXor(12).as('result')) + .select(floor('rating').as('floorRating')) ); expectResults(snapshot, { - result: 9 + floorRating: 4 }); }); - itIf(testUnsupportedFeatures)('supports Bit_not', async () => { - let snapshot = await execute( + it('can compute e to the power of a numeric value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) .limit(1) - .select( - bitNot(constant(Bytes.fromUint8Array(Uint8Array.of(0xfd)))).as( - 'result' - ) - ) + .select(field('rating').exp().as('expRating')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + expRating: 109.94717245212352 }); - snapshot = await execute( + }); + + it('can compute e to the power of a numeric value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0xfd))) - .bitNot() - .as('result') - ) + .select(exp('rating').as('expRating')) + ); + expect(snapshot.results[0].get('expRating')).to.be.approximately( + 109.94717245212351, + 0.000001 + ); + }); + + it('can compute the power of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').pow(2).as('powerRating')) + ); + expect(snapshot.results[0].get('powerRating')).to.be.approximately( + 17.64, + 0.0001 + ); + }); + + it('can compute the power of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(pow('rating', 2).as('powerRating')) + ); + expect(snapshot.results[0].get('powerRating')).to.be.approximately( + 17.64, + 0.0001 + ); + }); + + it('can round a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').round().as('roundedRating')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x02)) + roundedRating: 4 }); }); - itIf(testUnsupportedFeatures)('supports Bit_left_shift', async () => { - let snapshot = await execute( + it('can round a numeric value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select( - bitLeftShift( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), - 2 - ).as('result') - ) + .select(round('rating').as('roundedRating')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x04)) + roundedRating: 4 }); - snapshot = await execute( + }); + + it('can round a numeric value away from zero for positive half-way values', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) - .bitLeftShift(2) - .as('result') - ) + .addFields(constant(1.5).as('positiveHalf')) + .select(field('positiveHalf').round().as('roundedRating')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x04)) + roundedRating: 2 }); }); - itIf(testUnsupportedFeatures)('supports Bit_right_shift', async () => { - let snapshot = await execute( + it('can round a numeric value away from zero for negative half-way values', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select( - bitRightShift( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), - 2 - ).as('result') - ) + .addFields(constant(-1.5).as('negativeHalf')) + .select(field('negativeHalf').round().as('roundedRating')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + roundedRating: -2 }); - snapshot = await execute( + }); + + it('can get the collectionId from a path', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) - .bitRightShift(2) - .as('result') - ) + .select(field('__name__').collectionId().as('collectionId')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + collectionId: randomCol.id }); }); - itIf(testUnsupportedFeatures)('supports Document_id', async () => { - let snapshot = await execute( + it('can get the collectionId from a path with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .sort(field('rating').descending()) .limit(1) - .select(documentId(field('__path__')).as('docId')) + .select(collectionId('__name__').as('collectionId')) ); expectResults(snapshot, { - docId: 'book4' + collectionId: randomCol.id }); - snapshot = await execute( + }); + + it('can compute the length of a string value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .sort(field('rating').descending()) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(field('__path__').documentId().as('docId')) + .select(field('title').length().as('titleLength')) ); expectResults(snapshot, { - docId: 'book4' + titleLength: 36 }); }); - itIf(testUnsupportedFeatures)('supports Substr', async () => { - let snapshot = await execute( + it('can compute the length of a string value with the top-level function', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .sort(field('rating').descending()) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(substr('title', 9, 2).as('of')) + .select(length('title').as('titleLength')) ); expectResults(snapshot, { - of: 'of' + titleLength: 36 }); - snapshot = await execute( + }); + + it('can compute the length of an array value', async () => { + const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .sort(field('rating').descending()) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(field('title').substr(9, 2).as('of')) + .select(field('tags').length().as('tagsLength')) ); expectResults(snapshot, { - of: 'of' + tagsLength: 3 }); }); - itIf(testUnsupportedFeatures)( - 'supports Substr without length', - async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .sort(field('rating').descending()) - .limit(1) - .select(substr('title', 9).as('of')) - ); - expectResults(snapshot, { - of: 'of the Rings' - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .sort(field('rating').descending()) - .limit(1) - .select(field('title').substr(9).as('of')) - ); - expectResults(snapshot, { - of: 'of the Rings' - }); - } - ); - - itIf(testUnsupportedFeatures)('arrayConcat works', async () => { + it('can compute the length of an array value with the top-level function', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .select( - arrayConcat('tags', ['newTag1', 'newTag2'], field('tags'), [ - null - ]).as('modifiedTags') - ) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) + .select(length('tags').as('tagsLength')) ); expectResults(snapshot, { - modifiedTags: [ - 'comedy', - 'space', - 'adventure', - 'newTag1', - 'newTag2', - 'comedy', - 'space', - 'adventure', - null - ] + tagsLength: 3 }); }); - itIf(testUnsupportedFeatures)('testToLowercase', async () => { + it('can compute the length of a map value', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .select(toLower('title').as('lowercaseTitle')) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) + .select(field('awards').length().as('awardsLength')) ); expectResults(snapshot, { - lowercaseTitle: "the hitchhiker's guide to the galaxy" + awardsLength: 3 }); }); - itIf(testUnsupportedFeatures)('testToUppercase', async () => { + it('can compute the length of a vector value', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .select(toUpper('author').as('uppercaseAuthor')) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) + .select(field('embedding').length().as('embeddingLength')) ); - expectResults(snapshot, { uppercaseAuthor: 'DOUGLAS ADAMS' }); + expectResults(snapshot, { + embeddingLength: 10 + }); }); - itIf(testUnsupportedFeatures)('testTrim', async () => { + it('can compute the length of a bytes value', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .addFields( - constant(" The Hitchhiker's Guide to the Galaxy ").as('spacedTitle') - ) - .select(trim('spacedTitle').as('trimmedTitle'), field('spacedTitle')) + .select(constant('12é').as('value')) .limit(1) + .select(field('value').byteLength().as('valueLength')) ); expectResults(snapshot, { - spacedTitle: " The Hitchhiker's Guide to the Galaxy ", - trimmedTitle: "The Hitchhiker's Guide to the Galaxy" + valueLength: 4 }); }); - itIf(testUnsupportedFeatures)('test reverse', async () => { + it('can compute the natural logarithm of a numeric value', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where(eq('title', '1984')) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(reverse('title').as('reverseTitle')) + .select(field('rating').ln().as('lnRating')) ); - expectResults(snapshot, { title: '4891' }); + expect(snapshot.results[0]!.data().lnRating).to.be.closeTo(1.435, 0.001); }); - }); - describe('pagination', () => { + it('can compute the natural logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ln('rating').as('lnRating')) + ); + expect(snapshot.results[0]!.data().lnRating).to.be.closeTo(1.435, 0.001); + }); + + it('can compute the natural logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ln('rating').as('lnRating')) + ); + expectResults(snapshot, { + lnRating: 1.4350845252893227 + }); + }); + + it('can compute the logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').log(10).as('logRating')) + ); + expectResults(snapshot, { + logRating: 0.6232492903979004 + }); + }); + + it('can compute the logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(log('rating', 10).as('logRating')) + ); + expectResults(snapshot, { + logRating: 0.6232492903979004 + }); + }); + + it('can round a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(round('rating').as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can compute the square root of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').sqrt().as('sqrtRating')) + ); + expectResults(snapshot, { + sqrtRating: 2.04939015319192 + }); + }); + + it('can compute the square root of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(sqrt('rating').as('sqrtRating')) + ); + expectResults(snapshot, { + sqrtRating: 2.04939015319192 + }); + }); + + it('can reverse a string', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('title').reverse().as('reversedTitle')) + ); + expectResults(snapshot, { + reversedTitle: "yxalaG eht ot ediuG s'rekihhctiH ehT" + }); + }); + + it('can reverse a string with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(stringReverse('title').as('reversedTitle')) + ); + expectResults(snapshot, { + reversedTitle: "yxalaG eht ot ediuG s'rekihhctiH ehT" + }); + }); + + it('supports Document_id', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + documentId(field('__name__')).as('docId'), + documentId(field('__path__')).as('noDocId') + ) + ); + expectResults(snapshot, { + docId: 'book4', + noDocId: null + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('__name__').documentId().as('docId')) + ); + expectResults(snapshot, { + docId: 'book4' + }); + }); + + it('supports substring', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substring('title', 9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substring(9, 2).as('of')) + ); + expectResults(snapshot, { + of: 'of' + }); + }); + + it('supports substring without length', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(substring('title', 9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('title').substring(9).as('of')) + ); + expectResults(snapshot, { + of: 'of the Rings' + }); + }); + + it('test toLower', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('title')) + .select(toLower('author').as('lowercaseAuthor')) + .limit(1) + ); + expectResults(snapshot, { + lowercaseAuthor: 'george orwell' + }); + }); + + it('test toUpper', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(ascending('title')) + .select(toUpper('author').as('uppercaseAuthor')) + .limit(1) + ); + expectResults(snapshot, { uppercaseAuthor: 'GEORGE ORWELL' }); + }); + + it('testTrim', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + constant(" The Hitchhiker's Guide to the Galaxy ").as('spacedTitle') + ) + .select(trim('spacedTitle').as('trimmedTitle'), field('spacedTitle')) + .limit(1) + ); + expectResults(snapshot, { + spacedTitle: " The Hitchhiker's Guide to the Galaxy ", + trimmedTitle: "The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('test reverse', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', '1984')) + .limit(1) + .select(reverse('title').as('reverseTitle')) + ); + expectResults(snapshot, { reverseTitle: '4891' }); + }); + + // TODO(new-expression): Add new expression tests above this line + }); + + describe('pagination', () => { + let addedDocs: DocumentReference[] = []; + /** * Adds several books to the test collection. These * additional books support pagination test scenarios @@ -3275,7 +3741,9 @@ apiDescribe.only('Pipelines', persistence => { async function addBooks( collectionReference: CollectionReference ): Promise { - await setDoc(doc(collectionReference, 'book11'), { + let docRef = doc(collectionReference, 'book11'); + addedDocs.push(docRef); + await setDoc(docRef, { title: 'Jonathan Strange & Mr Norrell', author: 'Susanna Clarke', genre: 'Fantasy', @@ -3284,7 +3752,9 @@ apiDescribe.only('Pipelines', persistence => { tags: ['historical fantasy', 'magic', 'alternate history', 'england'], awards: { hugo: false, nebula: false } }); - await setDoc(doc(collectionReference, 'book12'), { + docRef = doc(collectionReference, 'book12'); + addedDocs.push(docRef); + await setDoc(docRef, { title: 'The Master and Margarita', author: 'Mikhail Bulgakov', genre: 'Satire', @@ -3298,7 +3768,9 @@ apiDescribe.only('Pipelines', persistence => { ], awards: {} }); - await setDoc(doc(collectionReference, 'book13'), { + docRef = doc(collectionReference, 'book13'); + addedDocs.push(docRef); + await setDoc(docRef, { title: 'A Long Way to a Small, Angry Planet', author: 'Becky Chambers', genre: 'Science Fiction', @@ -3309,125 +3781,133 @@ apiDescribe.only('Pipelines', persistence => { }); } - // sort on __name__ is not working, see b/409358591 - itIf(testUnsupportedFeatures)( - 'supports pagination with filters', - async () => { - await addBooks(randomCol); - const pageSize = 2; - const pipeline = firestore - .pipeline() - .collection(randomCol.path) - .select('title', 'rating', '__name__') - .sort(field('rating').descending(), field('__name__').ascending()); + afterEach(async () => { + for (let i = 0; i < addedDocs.length; i++) { + await deleteDoc(addedDocs[i]); + } + addedDocs = []; + }); - 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 pagination with filters', async () => { + await addBooks(randomCol); + const pageSize = 2; + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'rating', '__name__') + .sort(field('rating').descending(), field('__name__').ascending()); + + let snapshot = await execute(pipeline.limit(pageSize)); + expectResults( + snapshot, + { title: 'The Lord of the Rings', rating: 4.7 }, + { title: 'Dune', rating: 4.6 } + ); - const lastDoc = snapshot.results[snapshot.results.length - 1]; + const lastDoc = snapshot.results[snapshot.results.length - 1]; - snapshot = await execute( - pipeline - .where( - or( - and( - field('rating').eq(lastDoc.get('rating')), - field('__path__').gt(lastDoc.ref?.id) - ), - field('rating').lt(lastDoc.get('rating')) - ) + snapshot = await execute( + pipeline + .where( + or( + and( + field('rating').equal(lastDoc.get('rating')), + field('__name__').greaterThan(lastDoc.ref) + ), + field('rating').lessThan(lastDoc.get('rating')) ) - .limit(pageSize) - ); - expectResults( - snapshot, - { title: 'Pride and Prejudice', rating: 4.5 }, - { title: 'Crime and Punishment', rating: 4.3 } - ); - } - ); + ) + .limit(pageSize) + ); + expectResults( + snapshot, + { title: 'Jonathan Strange & Mr Norrell', rating: 4.6 }, + { title: 'The Master and Margarita', rating: 4.6 } + ); + }); - // sort on __name__ is not working, see b/409358591 - itIf(testUnsupportedFeatures)( - 'supports pagination with offsets', - async () => { - await addBooks(randomCol); + it('supports pagination with offsets', async () => { + await addBooks(randomCol); - const secondFilterField = '__path__'; + const secondFilterField = '__name__'; - const pipeline = firestore - .pipeline() - .collection(randomCol.path) - .select('title', 'rating', secondFilterField) - .sort( - field('rating').descending(), - field(secondFilterField).ascending() - ); + const pipeline = firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'rating', secondFilterField) + .sort( + field('rating').descending(), + field(secondFilterField).ascending() + ); - const pageSize = 2; - let currPage = 0; + const pageSize = 2; + let currPage = 0; - let snapshot = await execute( - pipeline.offset(currPage++ * pageSize).limit(pageSize) - ); + let snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); - expectResults( - snapshot, - { - title: 'The Lord of the Rings', - rating: 4.7 - }, - { title: 'Dune', rating: 4.6 } - ); + expectResults( + snapshot, + { + title: 'The Lord of the Rings', + rating: 4.7 + }, + { title: 'Dune', rating: 4.6 } + ); - snapshot = await execute( - pipeline.offset(currPage++ * pageSize).limit(pageSize) - ); - expectResults( - snapshot, - { - title: 'Jonathan Strange & Mr Norrell', - rating: 4.6 - }, - { title: 'The Master and Margarita', rating: 4.6 } - ); + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'Jonathan Strange & Mr Norrell', + rating: 4.6 + }, + { title: 'The Master and Margarita', rating: 4.6 } + ); - snapshot = await execute( - pipeline.offset(currPage++ * pageSize).limit(pageSize) - ); - expectResults( - snapshot, - { - title: 'A Long Way to a Small, Angry Planet', - rating: 4.6 - }, - { - title: 'Pride and Prejudice', - rating: 4.5 - } - ); - } - ); + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'A Long Way to a Small, Angry Planet', + rating: 4.6 + }, + { + title: 'Pride and Prejudice', + rating: 4.5 + } + ); + }); }); - describe('console support', () => { - it('supports internal serialization to proto', async () => { - const pipeline = firestore - .pipeline() - .collection('books') - .where(eq('awards.hugo', true)) - .select( - 'title', - field('nestedField.level.1'), - mapGet('nestedField', 'level.1').mapGet('level.2').as('nested') + describe('stage options', () => { + describe('forceIndex', () => { + // SKIP: requires pre-existing index + it.skip('Collection Stage', async () => { + const snapshot = await execute( + firestore.pipeline().collection({ + collection: randomCol, + forceIndex: 'unknown' + }) ); + expect(snapshot.results.length).to.equal(10); + }); - const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); - expect(proto).not.to.be.null; + // SKIP: requires pre-existing index + it.skip('CollectionGroup Stage', async () => { + const snapshot = await execute( + firestore.pipeline().collectionGroup({ + collectionId: randomCol.id, + forceIndex: 'unknown' + }) + ); + expect(snapshot.results.length).to.equal(10); + }); }); }); }); diff --git a/packages/firestore/test/lite/pipeline.test.ts b/packages/firestore/test/lite/pipeline.test.ts index 08f45b609df..c227e556e23 100644 --- a/packages/firestore/test/lite/pipeline.test.ts +++ b/packages/firestore/test/lite/pipeline.test.ts @@ -33,42 +33,34 @@ import { add, subtract, multiply, - avg, - bitAnd, - substr, - constantVector, - bitLeftShift, - bitNot, + average, + substring, count, mapMerge, mapRemove, - bitOr, ifError, isAbsent, isError, or, - rand, - bitRightShift, - bitXor, isNotNan, map, isNotNull, isNull, mod, documentId, - eq, - neq, - lt, + equal, + notEqual, + lessThan, countIf, - lte, - gt, + lessThanOrEqual, + greaterThan, arrayConcat, arrayContains, arrayContainsAny, - eqAny, - notEqAny, + equalAny, + notEqualAny, xor, - cond, + conditional, logicalMaximum, logicalMinimum, exists, @@ -77,7 +69,7 @@ import { like, regexContains, regexMatch, - strContains, + stringContains, startsWith, endsWith, mapGet, @@ -95,25 +87,24 @@ import { unixSecondsToTimestamp, timestampToUnixSeconds, timestampAdd, - timestampSub, + timestampSubtract, ascending, descending, - FunctionExpr, - BooleanExpr, + FunctionExpression, + BooleanExpression, AggregateFunction, sum, - strConcat, + stringConcat, arrayContainsAll, arrayLength, charLength, divide, - replaceFirst, - replaceAll, byteLength, not, toLower, toUpper, - trim, arrayGet + trim, + arrayGet } from '../../src/lite-api/expressions'; import { documentId as documentIdFieldPath } from '../../src/lite-api/field_path'; import { vector } from '../../src/lite-api/field_value_impl'; @@ -344,7 +335,6 @@ describe('Firestore Pipelines', () => { .sort(ascending('__name__')); const snapshot = await execute(ppl); expect(snapshot.results.length).to.equal(10); - expect(snapshot.pipeline).to.equal(ppl); expectResults( snapshot, 'book1', @@ -444,7 +434,7 @@ describe('Firestore Pipelines', () => { const pipeline = firestore .pipeline() .collection(randomCol.path) - .aggregate(avg('rating').as('avgRating')); + .aggregate(average('rating').as('avgRating')); const snapshot = await execute(pipeline); const end = new Date().valueOf(); @@ -462,7 +452,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .aggregate({ - accumulators: [avg('rating').as('avgRating')], + accumulators: [average('rating').as('avgRating')], groups: ['genre'] }); @@ -558,7 +548,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .database() - .where(eq('randomId', randomId)) + .where(equal('randomId', randomId)) .sort(ascending('order')) ); expectResults(snapshot, doc1.id, doc2.id); @@ -581,8 +571,7 @@ describe('Firestore Pipelines', () => { Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) ).as('bytes'), constant(doc(firestore, 'foo', 'bar')).as('documentReference'), - constantVector(vector([1, 2, 3])).as('vectorValue'), - constantVector([1, 2, 3]).as('vectorValue2'), + constant(vector([1, 2, 3])).as('vectorValue'), map({ 'number': 1, 'string': 'a string', @@ -745,7 +734,7 @@ describe('Firestore Pipelines', () => { ) .where( and( - eq('metadataArray', [ + equal('metadataArray', [ 1, 2, field('genre'), @@ -755,7 +744,7 @@ describe('Firestore Pipelines', () => { published: field('published') } ]), - eq('metadata', { + equal('metadata', { genre: field('genre'), rating: multiply('rating', 10), nestedArray: [field('title')], @@ -814,10 +803,10 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('genre', 'Science Fiction')) + .where(equal('genre', 'Science Fiction')) .aggregate( countAll().as('count'), - avg('rating').as('avgRating'), + average('rating').as('avgRating'), maximum('rating').as('maxRating'), sum('rating').as('sumRating') ) @@ -836,7 +825,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(lt('published', 1900)) + .where(lessThan('published', 1900)) .aggregate({ accumulators: [], groups: ['genre'] @@ -850,12 +839,12 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(lt(field('published'), 1984)) + .where(lessThan(field('published'), 1984)) .aggregate({ - accumulators: [avg('rating').as('avgRating')], + accumulators: [average('rating').as('avgRating')], groups: ['genre'] }) - .where(gt('avgRating', 4.3)) + .where(greaterThan('avgRating', 4.3)) .sort(field('avgRating').descending()) ); expectResults( @@ -866,7 +855,7 @@ describe('Firestore Pipelines', () => { ); }); - it('returns min, max, count, and countAll accumulations', async () => { + it('returns minimum, maximum, count, and countAll accumulations', async () => { const snapshot = await execute( firestore .pipeline() @@ -891,7 +880,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .aggregate(countIf(field('rating').gt(4.3)).as('count')) + .aggregate(countIf(field('rating').greaterThan(4.3)).as('count')) ); const expectedResults = { count: 3 @@ -902,7 +891,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .aggregate(field('rating').gt(4.3).countIf().as('count')) + .aggregate(field('rating').greaterThan(4.3).countIf().as('count')) ); expectResults(snapshot, expectedResults); }); @@ -1067,8 +1056,8 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) ) ) ); @@ -1081,9 +1070,9 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( and( - gt('rating', 4.5), - eqAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), - lt('published', 1965) + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lessThan('published', 1965) ) ) ); @@ -1096,9 +1085,9 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( or( - eq('genre', 'Romance'), - eq('genre', 'Dystopian'), - eq('genre', 'Fantasy') + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy') ) ) .sort(ascending('title')) @@ -1120,10 +1109,10 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( xor( - eq('genre', 'Romance'), - eq('genre', 'Dystopian'), - eq('genre', 'Fantasy'), - eq('published', 1949) + equal('genre', 'Romance'), + equal('genre', 'Dystopian'), + equal('genre', 'Fantasy'), + equal('published', 1949) ) ) .select('title') @@ -1157,7 +1146,7 @@ describe('Firestore Pipelines', () => { }); }); - describe('generic stage', () => { + describe('raw stage', () => { it('can select fields', async () => { const snapshot = await execute( firestore @@ -1192,7 +1181,7 @@ describe('Firestore Pipelines', () => { .select('title', 'author') .rawStage('add_fields', [ { - display: strConcat('title', ' - ', field('author')) + display: stringConcat('title', ' - ', field('author')) } ]) ); @@ -1209,7 +1198,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select('title', 'author') - .rawStage('where', [field('author').eq('Douglas Adams')]) + .rawStage('where', [field('author').equal('Douglas Adams')]) ); expectResults(snapshot, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1245,7 +1234,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .select('title', 'author', 'rating') .rawStage('aggregate', [ - { averageRating: field('rating').avg() }, + { averageRating: field('rating').average() }, {} ]) ); @@ -1296,7 +1285,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .replaceWith('awards') ); expectResults(snapshot, { @@ -1311,7 +1300,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .replaceWith( map({ foo: 'bar', @@ -1406,7 +1395,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .unnest(field('tags').as('tag')) .select( 'title', @@ -1474,7 +1463,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', "The Hitchhiker's Guide to the Galaxy")) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) .unnest(array([1, 2, 3]).as('copy')) .select( 'title', @@ -1604,7 +1593,7 @@ describe('Firestore Pipelines', () => { }); describe('function expressions', () => { - it('logical max works', async () => { + it('logical maximum works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1626,7 +1615,7 @@ describe('Firestore Pipelines', () => { ); }); - it('logical min works', async () => { + it('logical minimum works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1648,15 +1637,15 @@ describe('Firestore Pipelines', () => { ); }); - it('cond works', async () => { + it('conditiona works', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select( 'title', - cond( - lt(field('published'), 1960), + conditional( + lessThan(field('published'), 1960), constant(1960), field('published') ).as('published-safe') @@ -1677,7 +1666,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eqAny('published', [1979, 1999, 1967])) + .where(equalAny('published', [1979, 1999, 1967])) .sort(descending('title')) .select('title') ); @@ -1694,7 +1683,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .where( - notEqAny( + notEqualAny( 'published', [1965, 1925, 1949, 1960, 1866, 1985, 1954, 1967, 1979] ) @@ -1750,7 +1739,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select(arrayLength('tags').as('tagsCount')) - .where(eq('tagsCount', 3)) + .where(equal('tagsCount', 3)) ); expect(snapshot.results.length).to.equal(10); }); @@ -1762,7 +1751,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(ascending('author')) .select( - field('author').strConcat(' - ', field('title')).as('bookInfo') + field('author').stringConcat(' - ', field('title')).as('bookInfo') ) .limit(1) ); @@ -1810,7 +1799,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(strContains('title', "'s")) + .where(stringContains('title', "'s")) .select('title') .sort(field('title').ascending()) ); @@ -1827,7 +1816,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select(charLength('title').as('titleLength'), field('title')) - .where(gt('titleLength', 20)) + .where(greaterThan('titleLength', 20)) .sort(field('title').ascending()) ); @@ -1891,7 +1880,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('title', 'To Kill a Mockingbird')) + .where(equal('title', 'To Kill a Mockingbird')) .select( add(field('rating'), 1).as('ratingPlusOne'), subtract(field('published'), 1900).as('yearsSince1900'), @@ -1921,9 +1910,9 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( and( - gt('rating', 4.2), - lte(field('rating'), 4.5), - neq('genre', 'Science Fiction') + greaterThan('rating', 4.2), + lessThanOrEqual(field('rating'), 4.5), + notEqual('genre', 'Science Fiction') ) ) .select('rating', 'title') @@ -1947,8 +1936,11 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .where( or( - and(gt('rating', 4.5), eq('genre', 'Science Fiction')), - lt('published', 1900) + and( + greaterThan('rating', 4.5), + equal('genre', 'Science Fiction') + ), + lessThan('published', 1900) ) ) .select('title') @@ -1973,9 +1965,7 @@ describe('Firestore Pipelines', () => { isNull('rating').as('ratingIsNull'), isNan('rating').as('ratingIsNaN'), isError(arrayGet('title', 0)).as('isError'), - ifError(arrayGet('title', 0), constant('was error')).as( - 'ifError' - ), + ifError(arrayGet('title', 0), constant('was error')).as('ifError'), isAbsent('foo').as('isAbsent'), isNotNull('title').as('titleIsNotNull'), isNotNan('cost').as('costIsNotNan'), @@ -2005,9 +1995,7 @@ describe('Firestore Pipelines', () => { field('rating').isNull().as('ratingIsNull'), field('rating').isNan().as('ratingIsNaN'), arrayGet('title', 0).isError().as('isError'), - arrayGet('title', 0) - .ifError(constant('was error')) - .as('ifError'), + arrayGet('title', 0).ifError(constant('was error')).as('ifError'), field('foo').isAbsent().as('isAbsent'), field('title').isNotNull().as('titleIsNotNull'), field('cost').isNotNan().as('costIsNotNan') @@ -2035,7 +2023,7 @@ describe('Firestore Pipelines', () => { field('awards').mapGet('others').as('others'), field('title') ) - .where(eq('hugoAward', true)) + .where(equal('hugoAward', true)) ); expectResults( snapshot, @@ -2049,20 +2037,20 @@ describe('Firestore Pipelines', () => { }); it('testDistanceFunctions', async () => { - const sourceVector = [0.1, 0.1]; - const targetVector = [0.5, 0.8]; + const sourceVector = vector([0.1, 0.1]); + const targetVector = vector([0.5, 0.8]); let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .select( - cosineDistance(constantVector(sourceVector), targetVector).as( + cosineDistance(constant(sourceVector), targetVector).as( 'cosineDistance' ), - dotProduct(constantVector(sourceVector), targetVector).as( + dotProduct(constant(sourceVector), targetVector).as( 'dotProductDistance' ), - euclideanDistance(constantVector(sourceVector), targetVector).as( + euclideanDistance(constant(sourceVector), targetVector).as( 'euclideanDistance' ) ) @@ -2080,13 +2068,13 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .select( - constantVector(sourceVector) + constant(sourceVector) .cosineDistance(targetVector) .as('cosineDistance'), - constantVector(sourceVector) + constant(sourceVector) .dotProduct(targetVector) .as('dotProductDistance'), - constantVector(sourceVector) + constant(sourceVector) .euclideanDistance(targetVector) .as('euclideanDistance') ) @@ -2106,7 +2094,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .limit(1) - .select(vectorLength(constantVector([1, 2, 3])).as('vectorLength')) + .select(vectorLength(constant(vector([1, 2, 3]))).as('vectorLength')) ); expectResults(snapshot, { vectorLength: 3 @@ -2118,7 +2106,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('awards.hugo', true)) + .where(equal('awards.hugo', true)) .sort(descending('title')) .select('title', 'awards.hugo') ); @@ -2137,7 +2125,7 @@ describe('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(eq('awards.hugo', true)) + .where(equal('awards.hugo', true)) .select( 'title', field('nestedField.level.1'), @@ -2165,7 +2153,7 @@ describe('Firestore Pipelines', () => { .sort(descending('rating')) .limit(1) .select( - new FunctionExpr('add', [field('rating'), constant(1)]).as( + new FunctionExpression('add', [field('rating'), constant(1)]).as( 'rating' ) ) @@ -2181,9 +2169,9 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .where( - new BooleanExpr('and', [ - field('rating').gt(0), - field('title').charLength().lt(5), + new BooleanExpression('and', [ + field('rating').greaterThan(0), + field('title').charLength().lessThan(5), field('tags').arrayContains('propaganda') ]) ) @@ -2200,7 +2188,7 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .where( - new BooleanExpr('array_contains_any', [ + new BooleanExpression('array_contains_any', [ field('tags'), array(['politics']) ]) @@ -2218,9 +2206,9 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .aggregate( - new AggregateFunction('count_if', [field('rating').gte(4.5)]).as( - 'countOfBest' - ) + new AggregateFunction('count_if', [ + field('rating').greaterThanOrEqual(4.5) + ]).as('countOfBest') ) ); expectResults(snapshot, { @@ -2234,7 +2222,9 @@ describe('Firestore Pipelines', () => { .pipeline() .collection(randomCol.path) .sort( - new FunctionExpr('char_length', [field('title')]).ascending(), + new FunctionExpression('char_length', [ + field('title') + ]).ascending(), descending('__name__') ) .limit(3) @@ -2255,45 +2245,6 @@ describe('Firestore Pipelines', () => { }); }); - itIf(testUnsupportedFeatures)('testReplaceFirst', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', 'The Lord of the Rings')) - .limit(1) - .select(replaceFirst('title', 'o', '0').as('newName')) - ); - expectResults(snapshot, { newName: 'The L0rd of the Rings' }); - }); - - itIf(testUnsupportedFeatures)('testReplaceAll', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .where(eq('title', 'The Lord of the Rings')) - .limit(1) - .select(replaceAll('title', 'o', '0').as('newName')) - ); - expectResults(snapshot, { newName: 'The L0rd 0f the Rings' }); - }); - - it('supports Rand', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(10) - .select(rand().as('result')) - ); - expect(snapshot.results.length).to.equal(10); - snapshot.results.forEach(d => { - expect(d.get('result')).to.be.lt(1); - expect(d.get('result')).to.be.gte(0); - }); - }); - it('supports array', async () => { const snapshot = await execute( firestore @@ -2506,12 +2457,16 @@ describe('Firestore Pipelines', () => { timestampAdd('timestamp', 'second', 10).as('plus10seconds'), timestampAdd('timestamp', 'microsecond', 10).as('plus10micros'), timestampAdd('timestamp', 'millisecond', 10).as('plus10millis'), - timestampSub('timestamp', 'day', 10).as('minus10days'), - timestampSub('timestamp', 'hour', 10).as('minus10hours'), - timestampSub('timestamp', 'minute', 10).as('minus10minutes'), - timestampSub('timestamp', 'second', 10).as('minus10seconds'), - timestampSub('timestamp', 'microsecond', 10).as('minus10micros'), - timestampSub('timestamp', 'millisecond', 10).as('minus10millis') + timestampSubtract('timestamp', 'day', 10).as('minus10days'), + timestampSubtract('timestamp', 'hour', 10).as('minus10hours'), + timestampSubtract('timestamp', 'minute', 10).as('minus10minutes'), + timestampSubtract('timestamp', 'second', 10).as('minus10seconds'), + timestampSubtract('timestamp', 'microsecond', 10).as( + 'minus10micros' + ), + timestampSubtract('timestamp', 'millisecond', 10).as( + 'minus10millis' + ) ) ); expectResults(snapshot, { @@ -2528,7 +2483,7 @@ describe('Firestore Pipelines', () => { minus10micros: new Timestamp(1741380234, 999990000), minus10millis: new Timestamp(1741380234, 990000000) }); - }); + }).timeout(10000); it('supports byteLength', async () => { const snapshot = await execute( @@ -2556,7 +2511,7 @@ describe('Firestore Pipelines', () => { .collection(randomCol) .limit(1) .select(constant(true).as('trueField')) - .select('trueField', not(eq('trueField', true)).as('falseField')) + .select('trueField', not(equal('trueField', true)).as('falseField')) ); expectResults(snapshot, { @@ -2564,187 +2519,43 @@ describe('Firestore Pipelines', () => { falseField: false }); }); - }); - - describe('not yet implemented in backend', () => { - itIf(testUnsupportedFeatures)('supports Bit_and', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(bitAnd(constant(5), 12).as('result')) - ); - expectResults(snapshot, { - result: 4 - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_and', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(constant(5).bitAnd(12).as('result')) - ); - expectResults(snapshot, { - result: 4 - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_or', async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(bitOr(constant(5), 12).as('result')) - ); - expectResults(snapshot, { - result: 13 - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(constant(5).bitOr(12).as('result')) - ); - expectResults(snapshot, { - result: 13 - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_xor', async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(bitXor(constant(5), 12).as('result')) - ); - expectResults(snapshot, { - result: 9 - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(constant(5).bitXor(12).as('result')) - ); - expectResults(snapshot, { - result: 9 - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_not', async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select( - bitNot(constant(Bytes.fromUint8Array(Uint8Array.of(0xfd)))).as( - 'result' - ) - ) - ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x02)) - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0xfd))) - .bitNot() - .as('result') - ) - ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x02)) - }); - }); - itIf(testUnsupportedFeatures)('supports Bit_left_shift', async () => { + it('supports Document_id', async () => { let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .sort(field('rating').descending()) .limit(1) - .select( - bitLeftShift( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), - 2 - ).as('result') - ) - ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x04)) - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) - .bitLeftShift(2) - .as('result') - ) - ); - expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x04)) - }); - }); - - itIf(testUnsupportedFeatures)('supports Bit_right_shift', async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select( - bitRightShift( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))), - 2 - ).as('result') - ) + .select(documentId(field('__path__')).as('docId')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + docId: 'book4' }); snapshot = await execute( firestore .pipeline() .collection(randomCol.path) + .sort(field('rating').descending()) .limit(1) - .select( - constant(Bytes.fromUint8Array(Uint8Array.of(0x02))) - .bitRightShift(2) - .as('result') - ) + .select(field('__path__').documentId().as('docId')) ); expectResults(snapshot, { - result: Bytes.fromUint8Array(Uint8Array.of(0x01)) + docId: 'book4' }); }); - itIf(testUnsupportedFeatures)('supports Document_id', async () => { + it('supports Substr', async () => { let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(documentId(field('__path__')).as('docId')) + .select(substring('title', 9, 2).as('of')) ); expectResults(snapshot, { - docId: 'book4' + of: 'of' }); snapshot = await execute( firestore @@ -2752,24 +2563,24 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(field('__path__').documentId().as('docId')) + .select(field('title').substring(9, 2).as('of')) ); expectResults(snapshot, { - docId: 'book4' + of: 'of' }); }); - itIf(testUnsupportedFeatures)('supports Substr', async () => { + 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, 2).as('of')) + .select(substring('title', 9).as('of')) ); expectResults(snapshot, { - of: 'of' + of: 'of the Rings' }); snapshot = await execute( firestore @@ -2777,42 +2588,14 @@ describe('Firestore Pipelines', () => { .collection(randomCol.path) .sort(field('rating').descending()) .limit(1) - .select(field('title').substr(9, 2).as('of')) + .select(field('title').substring(9).as('of')) ); expectResults(snapshot, { - of: 'of' + of: 'of the Rings' }); }); - itIf(testUnsupportedFeatures)( - 'supports Substr without length', - async () => { - let snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .sort(field('rating').descending()) - .limit(1) - .select(substr('title', 9).as('of')) - ); - expectResults(snapshot, { - of: 'of the Rings' - }); - snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .sort(field('rating').descending()) - .limit(1) - .select(field('title').substr(9).as('of')) - ); - expectResults(snapshot, { - of: 'of the Rings' - }); - } - ); - - itIf(testUnsupportedFeatures)('arrayConcat works', async () => { + it('arrayConcat works', async () => { const snapshot = await execute( firestore .pipeline() @@ -2839,7 +2622,7 @@ describe('Firestore Pipelines', () => { }); }); - itIf(testUnsupportedFeatures)('testToLowercase', async () => { + it('testToLowercase', async () => { const snapshot = await execute( firestore .pipeline() @@ -2852,7 +2635,7 @@ describe('Firestore Pipelines', () => { }); }); - itIf(testUnsupportedFeatures)('testToUppercase', async () => { + it('testToUppercase', async () => { const snapshot = await execute( firestore .pipeline() @@ -2863,7 +2646,7 @@ describe('Firestore Pipelines', () => { expectResults(snapshot, { uppercaseAuthor: 'DOUGLAS ADAMS' }); }); - itIf(testUnsupportedFeatures)('testTrim', async () => { + it('testTrim', async () => { const snapshot = await execute( firestore .pipeline() @@ -2880,12 +2663,12 @@ describe('Firestore Pipelines', () => { }); }); - itIf(testUnsupportedFeatures)('test reverse', async () => { + it('test reverse', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where(eq('title', '1984')) + .where(equal('title', '1984')) .limit(1) .select(reverse('title').as('reverseTitle')) ); @@ -2964,10 +2747,10 @@ describe('Firestore Pipelines', () => { .where( or( and( - field('rating').eq(lastDoc.get('rating')), - field('__path__').gt(lastDoc.ref?.id) + field('rating').equal(lastDoc.get('rating')), + field('__path__').greaterThan(lastDoc.ref?.id) ), - field('rating').lt(lastDoc.get('rating')) + field('rating').lessThan(lastDoc.get('rating')) ) ) .limit(pageSize) diff --git a/packages/firestore/test/unit/api/pipeline_impl.test.ts b/packages/firestore/test/unit/api/pipeline_impl.test.ts index 32476e6bee9..f11db5a427e 100644 --- a/packages/firestore/test/unit/api/pipeline_impl.test.ts +++ b/packages/firestore/test/unit/api/pipeline_impl.test.ts @@ -161,13 +161,13 @@ describe('execute(Pipeline|PipelineOptions)', () => { ); }); - it('serializes the pipeline generic options', async () => { + it('serializes the pipeline raw options', async () => { const firestore = newTestFirestore(); const spy = fakePipelineResponse(firestore); await execute({ pipeline: firestore.pipeline().collection('foo'), - customOptions: { + rawOptions: { 'foo': 'bar' } }); diff --git a/packages/firestore/test/unit/core/options_util.test.ts b/packages/firestore/test/unit/core/options_util.test.ts index 523f3e4c11f..0549c2c8a9c 100644 --- a/packages/firestore/test/unit/core/options_util.test.ts +++ b/packages/firestore/test/unit/core/options_util.test.ts @@ -15,19 +15,20 @@ * limitations under the License. */ -import {expect} from "chai"; +import { expect } from 'chai'; -import {ParseContext} from "../../../src/api/parse_context"; -import {OptionsUtil} from "../../../src/core/options_util"; -import { - UserDataSource -} from "../../../src/lite-api/user_data_reader"; -import {testUserDataReader} from "../../util/helpers"; +import { ParseContext } from '../../../src/api/parse_context'; +import { OptionsUtil } from '../../../src/core/options_util'; +import { UserDataSource } from '../../../src/lite-api/user_data_reader'; +import { testUserDataReader } from '../../util/helpers'; describe('OptionsUtil', () => { let context: ParseContext | undefined; beforeEach(async () => { - context = testUserDataReader(false).createContext(UserDataSource.Argument, 'beforeEach'); + context = testUserDataReader(false).createContext( + UserDataSource.Argument, + 'beforeEach' + ); }); afterEach(async () => { @@ -37,33 +38,28 @@ describe('OptionsUtil', () => { it('should support known options', () => { const optionsUtil = new OptionsUtil({ fooBar: { - serverName: 'foo_bar', - }, + serverName: 'foo_bar' + } }); - const proto = optionsUtil.getOptionsProto( - context!, { - fooBar: 'recommended', + const proto = optionsUtil.getOptionsProto(context!, { + fooBar: 'recommended' }); expect(proto).deep.equal({ 'foo_bar': { - stringValue: 'recommended', - }, + stringValue: 'recommended' + } }); }); it('should support unknown options', () => { const optionsUtil = new OptionsUtil({}); - const proto = optionsUtil.getOptionsProto( - context!, - {}, - {baz: 'foo'} - ); + const proto = optionsUtil.getOptionsProto(context!, {}, { baz: 'foo' }); expect(proto).to.deep.equal({ baz: { - stringValue: 'foo', - }, + stringValue: 'foo' + } }); }); @@ -72,40 +68,40 @@ describe('OptionsUtil', () => { const proto = optionsUtil.getOptionsProto( context!, {}, - {'foo.bar': 'baz'} + { 'foo.bar': 'baz' } ); expect(proto).to.deep.equal({ foo: { mapValue: { fields: { - bar: {stringValue: 'baz'}, - }, - }, - }, + bar: { stringValue: 'baz' } + } + } + } }); }); it('should support options override', () => { const optionsUtil = new OptionsUtil({ indexMode: { - serverName: 'index_mode', - }, + serverName: 'index_mode' + } }); const proto = optionsUtil.getOptionsProto( context!, { - indexMode: 'recommended', + indexMode: 'recommended' }, { - 'index_mode': 'baz', + 'index_mode': 'baz' } ); expect(proto).to.deep.equal({ 'index_mode': { - stringValue: 'baz', - }, + stringValue: 'baz' + } }); }); @@ -115,22 +111,22 @@ describe('OptionsUtil', () => { serverName: 'foo', nestedOptions: { bar: { - serverName: 'bar', + serverName: 'bar' }, waldo: { - serverName: 'waldo', - }, - }, - }, + serverName: 'waldo' + } + } + } }); const proto = optionsUtil.getOptionsProto( context!, { - foo: {bar: 'yep', waldo: 'found'}, + foo: { bar: 'yep', waldo: 'found' } }, { 'foo.bar': 123, - 'foo.baz': true, + 'foo.baz': true } ); @@ -139,17 +135,17 @@ describe('OptionsUtil', () => { mapValue: { fields: { bar: { - integerValue: '123', + integerValue: '123' }, waldo: { - stringValue: 'found', + stringValue: 'found' }, baz: { - booleanValue: true, - }, - }, - }, - }, + booleanValue: true + } + } + } + } }); }); @@ -159,23 +155,23 @@ describe('OptionsUtil', () => { serverName: 'foo', nestedOptions: { bar: { - serverName: 'bar', + serverName: 'bar' }, waldo: { - serverName: 'waldo', - }, - }, - }, + serverName: 'waldo' + } + } + } }); const proto = optionsUtil.getOptionsProto( context!, { - foo: {bar: 'yep', waldo: 'found'}, + foo: { bar: 'yep', waldo: 'found' } }, { foo: { - bar: 123, - }, + bar: 123 + } } ); @@ -184,29 +180,29 @@ describe('OptionsUtil', () => { mapValue: { fields: { bar: { - integerValue: '123', - }, - }, - }, - }, + integerValue: '123' + } + } + } + } }); }); it('will replace a top level property that is not an object if given a nested field with dot notation', () => { const optionsUtil = new OptionsUtil({ foo: { - serverName: 'foo', - }, + serverName: 'foo' + } }); const proto = optionsUtil.getOptionsProto( context!, { - foo: 'bar', + foo: 'bar' }, { 'foo.bar': '123', - 'foo.waldo': true, + 'foo.waldo': true } ); @@ -215,14 +211,14 @@ describe('OptionsUtil', () => { mapValue: { fields: { bar: { - stringValue: '123', + stringValue: '123' }, waldo: { - booleanValue: true, - }, - }, - }, - }, + booleanValue: true + } + } + } + } }); }); }); diff --git a/packages/firestore/test/unit/core/structured_pipeline.test.ts b/packages/firestore/test/unit/core/structured_pipeline.test.ts index 712b24b1ae9..759dfecea44 100644 --- a/packages/firestore/test/unit/core/structured_pipeline.test.ts +++ b/packages/firestore/test/unit/core/structured_pipeline.test.ts @@ -15,15 +15,21 @@ * limitations under the License. */ -import {expect} from 'chai'; +import { expect } from 'chai'; import * as sinon from 'sinon'; -import {DatabaseId} from '../../../src/core/database_info'; -import {StructuredPipeline, StructuredPipelineOptions} from '../../../src/core/structured_pipeline'; -import {UserDataSource} from "../../../src/lite-api/user_data_reader"; -import {Pipeline as PipelineProto} from '../../../src/protos/firestore_proto_api'; -import {JsonProtoSerializer, ProtoSerializable} from '../../../src/remote/serializer'; -import {testUserDataReader} from "../../util/helpers"; +import { DatabaseId } from '../../../src/core/database_info'; +import { + StructuredPipeline, + StructuredPipelineOptions +} from '../../../src/core/structured_pipeline'; +import { UserDataSource } from '../../../src/lite-api/user_data_reader'; +import { Pipeline as PipelineProto } from '../../../src/protos/firestore_proto_api'; +import { + JsonProtoSerializer, + ProtoSerializable +} from '../../../src/remote/serializer'; +import { testUserDataReader } from '../../util/helpers'; describe('StructuredPipeline', () => { it('should serialize the pipeline argument', () => { @@ -31,8 +37,13 @@ describe('StructuredPipeline', () => { _toProto: sinon.fake.returns({} as PipelineProto) }; const structuredPipelineOptions = new StructuredPipelineOptions(); - structuredPipelineOptions._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); - const structuredPipeline = new StructuredPipeline(pipeline, structuredPipelineOptions); + structuredPipelineOptions._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') + ); + const structuredPipeline = new StructuredPipeline( + pipeline, + structuredPipelineOptions + ); const proto = structuredPipeline._toProto( new JsonProtoSerializer(DatabaseId.empty(), false) @@ -54,10 +65,10 @@ describe('StructuredPipeline', () => { const options = new StructuredPipelineOptions({ indexMode: 'recommended' }); - options._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); - const structuredPipeline = new StructuredPipeline( - pipeline,options + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') ); + const structuredPipeline = new StructuredPipeline(pipeline, options); const proto = structuredPipeline._toProto( new JsonProtoSerializer(DatabaseId.empty(), false) @@ -79,17 +90,16 @@ describe('StructuredPipeline', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; - const options = - new StructuredPipelineOptions({}, - { - 'foo_bar': 'baz' - } - ); - options._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); - const structuredPipeline = new StructuredPipeline( - pipeline, - options + const options = new StructuredPipelineOptions( + {}, + { + 'foo_bar': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') ); + const structuredPipeline = new StructuredPipeline(pipeline, options); const proto = structuredPipeline._toProto( new JsonProtoSerializer(DatabaseId.empty(), false) @@ -111,17 +121,16 @@ describe('StructuredPipeline', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; - const options = - new StructuredPipelineOptions({}, - { - 'foo.bar': 'baz' - } - ); - options._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); - const structuredPipeline = new StructuredPipeline( - pipeline, - options + const options = new StructuredPipelineOptions( + {}, + { + 'foo.bar': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') ); + const structuredPipeline = new StructuredPipeline(pipeline, options); const proto = structuredPipeline._toProto( new JsonProtoSerializer(DatabaseId.empty(), false) @@ -147,19 +156,18 @@ describe('StructuredPipeline', () => { const pipeline: ProtoSerializable = { _toProto: sinon.fake.returns({} as PipelineProto) }; - const options = - new StructuredPipelineOptions({ - indexMode: 'recommended' - }, - { - 'index_mode': 'baz' - } - ); - options._readUserData(testUserDataReader(false).createContext(UserDataSource.Argument, 'test')); - const structuredPipeline = new StructuredPipeline( - pipeline, - options + const options = new StructuredPipelineOptions( + { + indexMode: 'recommended' + }, + { + 'index_mode': 'baz' + } + ); + options._readUserData( + testUserDataReader(false).createContext(UserDataSource.Argument, 'test') ); + const structuredPipeline = new StructuredPipeline(pipeline, options); const proto = structuredPipeline._toProto( new JsonProtoSerializer(DatabaseId.empty(), false) From f07a909ea20e8de73244d458f43346c0f6c0b277 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:48:42 -0600 Subject: [PATCH 09/25] Cleanup --- .../firestore/lite/pipelines/pipelines.ts | 1 - packages/firestore/src/api_pipelines.ts | 1 - .../firestore/src/lite-api/expressions.ts | 30 ++----------------- .../test/integration/api/pipeline.test.ts | 16 +++++++--- 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/packages/firestore/lite/pipelines/pipelines.ts b/packages/firestore/lite/pipelines/pipelines.ts index 6e4c0c994ff..1b46bcbff7b 100644 --- a/packages/firestore/lite/pipelines/pipelines.ts +++ b/packages/firestore/lite/pipelines/pipelines.ts @@ -101,7 +101,6 @@ export { isAbsent, isError, or, - rand, divide, isNotNan, map, diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index d7e1dc3dc64..65338462a60 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -136,7 +136,6 @@ export { ascending, descending, countIf, - rand, array, arrayGet, isError, diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 53f9ecf592a..6128068e896 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -2167,14 +2167,6 @@ export class AggregateWithAlias implements UserData { readonly _methodName: string | undefined ) {} - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; - /** * @private * @internal @@ -2191,14 +2183,6 @@ export class AliasedExpression implements Selectable, UserData { exprType: ExpressionType = 'AliasedExpression'; selectable = true as const; - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; - constructor( readonly expr: Expression, readonly alias: string, @@ -2396,7 +2380,7 @@ export class Constant extends Expression { * @private * @internal */ - _toProto(serializer: JsonProtoSerializer): ProtoValue { + _toProto(_: JsonProtoSerializer): ProtoValue { hardAssert( this._protoValue !== undefined, 0x00ed, @@ -7064,19 +7048,11 @@ export function descending(field: Expression | string): Ordering { */ export class Ordering implements ProtoValueSerializable, UserData { constructor( - readonly expr: Expression, - readonly direction: 'ascending' | 'descending', + public readonly expr: Expression, + public readonly direction: 'ascending' | 'descending', readonly _methodName: string | undefined ) {} - /** - * @internal - * @private - * Indicates if this expression was created from a literal value passed - * by the caller. - */ - _createdFromLiteral: boolean = false; - /** * @private * @internal diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index fe1004ac81f..065432572b6 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -2231,16 +2231,24 @@ apiDescribe.only('Pipelines', persistence => { lessThan(field('published'), 1960), constant(1960), field('published') - ).as('published-safe') + ).as('published-safe'), + field('rating') + .greaterThanOrEqual(4.5) + .conditional(constant('great'), constant('good')) + .as('rating') ) .sort(field('title').ascending()) .limit(3) ); expectResults( snapshot, - { title: '1984', 'published-safe': 1960 }, - { title: 'Crime and Punishment', 'published-safe': 1960 }, - { title: 'Dune', 'published-safe': 1965 } + { title: '1984', 'published-safe': 1960, rating: 'good' }, + { + title: 'Crime and Punishment', + 'published-safe': 1960, + rating: 'good' + }, + { title: 'Dune', 'published-safe': 1965, rating: 'great' } ); }); From f9fef48ce49b7c3fc9ed47f5c2146c04c61345bf Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:49:18 -0600 Subject: [PATCH 10/25] Update constant(boolean) to return BooleanExpression --- .../firestore/src/lite-api/expressions.ts | 72 +++++++++++++++++-- .../test/integration/api/pipeline.test.ts | 24 +++++++ 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 6128068e896..d5d5308e46e 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -2422,12 +2422,12 @@ export function constant(value: number): Expression; export function constant(value: string): Expression; /** - * Creates a `Constant` instance for a boolean value. + * Creates a `BooleanExpression` instance for a boolean value. * * @param value The boolean value. * @return A new `Constant` instance. */ -export function constant(value: boolean): Expression; +export function constant(value: boolean): BooleanExpression; /** * Creates a `Constant` instance for a null value. @@ -2495,8 +2495,8 @@ export function constant(value: ProtoValue): Expression; */ export function constant(value: VectorValue): Expression; -export function constant(value: unknown): Expression { - return _constant(value, 'contant'); +export function constant(value: unknown): Expression | BooleanExpression { + return _constant(value, 'constant'); } /** @@ -2508,8 +2508,12 @@ export function constant(value: unknown): Expression { export function _constant( value: unknown, methodName: string | undefined -): Constant { - return new Constant(value, methodName); +): Constant | BooleanExpression { + if (typeof value === 'boolean') { + return new BooleanConstant(value, methodName); + } else { + return new Constant(value, methodName); + } } /** @@ -2630,6 +2634,62 @@ export class BooleanExpression extends FunctionExpression { not(): BooleanExpression { return new BooleanExpression('not', [this], 'not'); } + + /** + * Creates a conditional expression that evaluates to the 'then' expression + * if `this` expression evaluates to `true`, + * or evaluates to the 'else' expression if `this` expressions evaluates `false`. + * + * ```typescript + * // If 'age' is greater than 18, return "Adult"; otherwise, return "Minor". + * field("age").greaterThanOrEqual(18).conditional(constant("Adult"), constant("Minor")); + * ``` + * + * @param thenExpr The expression to evaluate if the condition is true. + * @param elseExpr The expression to evaluate if the condition is false. + * @return A new {@code Expr} representing the conditional expression. + */ + conditional(thenExpr: Expression, elseExpr: Expression): FunctionExpression { + return new FunctionExpression( + 'conditional', + [this, thenExpr, elseExpr], + 'conditional' + ); + } +} + +/** + * @private + * @internal + * + * To return a BooleanExpr as a constant, we need to break the pattern that expects a BooleanExpr to be a + * "pipeline function". Instead of building on serialization logic built into BooleanExpr, + * we override methods with those of an internally kept Constant value. + */ +export class BooleanConstant extends BooleanExpression { + private readonly _internalConstant: Constant; + + constructor(value: boolean, readonly _methodName?: string) { + super('', []); + + this._internalConstant = new Constant(value, _methodName); + } + + /** + * @private + * @internal + */ + _toProto(serializer: JsonProtoSerializer): ProtoValue { + return this._internalConstant._toProto(serializer); + } + + /** + * @private + * @internal + */ + _readUserData(context: ParseContext): void { + return this._internalConstant._readUserData(context); + } } /** diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 065432572b6..677f53e595c 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -813,6 +813,30 @@ apiDescribe.only('Pipelines', persistence => { } }); }); + + it('supports boolean value constants as a BooleanExpression', async () => { + const snapshots = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + conditional(constant(true), constant('TRUE'), constant('FALSE')).as( + 'true' + ), + conditional( + constant(false), + constant('TRUE'), + constant('FALSE') + ).as('false') + ) + ); + + expectResults(snapshots, { + 'true': 'TRUE', + 'false': 'FALSE' + }); + }); }); describe('stages', () => { From f0cc89b60f8877799eb7fcf406789f07fd25db13 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:34:32 -0600 Subject: [PATCH 11/25] Add ifError overloads for BooleanExpression --- .../firestore/src/lite-api/expressions.ts | 54 ++++++++++++++++++- .../test/integration/api/pipeline.test.ts | 8 +++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index d5d5308e46e..aaacdd3ffc1 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -2656,6 +2656,30 @@ export class BooleanExpression extends FunctionExpression { 'conditional' ); } + + /** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of this expression. + * + * ```typescript + * // Create an expression that protects against a divide by zero error + * // but always returns a boolean expression. + * constant(50).divide('length').gt(1).ifError(constant(false)); + * ``` + * + * @param catchValue The value that will be returned if this expression + * produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ + ifError(catchValue: BooleanExpression): BooleanExpression { + return new BooleanExpression( + 'if_error', + [this, catchValue], + 'ifError' + ); + } } /** @@ -2814,6 +2838,30 @@ export function isError(value: Expression): BooleanExpression { return value.isError(); } +/** + * @beta + * + * Creates an expression that returns the `catch` argument if there is an + * error, else return the result of the `try` argument evaluation. + * + * This overload is useful when a BooleanExpression is required. + * + * ```typescript + * // Create an expression that protects against a divide by zero error + * // but always returns a boolean expression. + * ifError(constant(50).divide('length').gt(1), constant(false)); + * ``` + * + * @param tryExpr The try expression. + * @param catchExpr The catch expression that will be evaluated and + * returned if the tryExpr produces an error. + * @return A new {@code Expr} representing the 'ifError' operation. + */ +export function ifError( + tryExpr: BooleanExpression, + catchExpr: BooleanExpression +): BooleanExpression; + /** * @beta * @@ -2861,7 +2909,11 @@ export function ifError( tryExpr: Expression, catchValue: unknown ): FunctionExpression { - return tryExpr.ifError(valueToDefaultExpr(catchValue)); + if (tryExpr instanceof BooleanExpression && catchValue instanceof BooleanExpression) { + return tryExpr.ifError(catchValue); + } else { + return tryExpr.ifError(valueToDefaultExpr(catchValue)); + } } /** diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 677f53e595c..711582856c7 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -2583,6 +2583,9 @@ apiDescribe.only('Pipelines', persistence => { ifError(divide(constant(1), constant(0)), constant('was error')).as( 'ifError' ), + ifError(divide(constant(1), constant(0)).greaterThan(1), constant(true)).not().as( + 'ifErrorBooleanExpression' + ), isAbsent('foo').as('isAbsent'), isNotNull('title').as('titleIsNotNull'), isNotNan('cost').as('costIsNotNan'), @@ -2595,6 +2598,7 @@ apiDescribe.only('Pipelines', persistence => { ratingIsNaN: false, isError: true, ifError: 'was error', + ifErrorBooleanExpression: false, isAbsent: true, titleIsNotNull: true, costIsNotNan: false, @@ -2615,6 +2619,9 @@ apiDescribe.only('Pipelines', persistence => { divide(constant(1), constant(0)) .ifError(constant('was error')) .as('ifError'), + divide(constant(1), constant(0)).greaterThan(1).ifError(constant(true)).not().as( + 'ifErrorBooleanExpression' + ), field('foo').isAbsent().as('isAbsent'), field('title').isNotNull().as('titleIsNotNull'), field('cost').isNotNan().as('costIsNotNan') @@ -2625,6 +2632,7 @@ apiDescribe.only('Pipelines', persistence => { ratingIsNaN: false, isError: true, ifError: 'was error', + ifErrorBooleanExpression: false, isAbsent: true, titleIsNotNull: true, costIsNotNan: false From e261a1a06bbeae2322e7d835e2e2738a53654470 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:04:51 -0600 Subject: [PATCH 12/25] Add abs expression and did formatting --- .../firestore/src/lite-api/expressions.ts | 44 ++++++++++++++++--- .../test/integration/api/pipeline.test.ts | 44 ++++++++++++++++--- packages/firestore/test/lite/pipeline.test.ts | 16 ++++++- 3 files changed, 90 insertions(+), 14 deletions(-) diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index aaacdd3ffc1..986138cb473 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -1102,6 +1102,20 @@ export abstract class Expression implements ProtoValueSerializable, UserData { return new FunctionExpression('floor', [this]); } + /** + * Creates an expression that computes the absolute value of a numeric value. + * + * ```typescript + * // Compute the absolute value of the 'price' field. + * field("price").abs(); + * ``` + * + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ + abs(): FunctionExpression { + return new FunctionExpression('abs', [this]); + } + /** * Creates an expression that computes e to the power of this expression. * @@ -2674,11 +2688,7 @@ export class BooleanExpression extends FunctionExpression { * @return A new {@code Expr} representing the 'ifError' operation. */ ifError(catchValue: BooleanExpression): BooleanExpression { - return new BooleanExpression( - 'if_error', - [this, catchValue], - 'ifError' - ); + return new BooleanExpression('if_error', [this, catchValue], 'ifError'); } } @@ -2909,7 +2919,10 @@ export function ifError( tryExpr: Expression, catchValue: unknown ): FunctionExpression { - if (tryExpr instanceof BooleanExpression && catchValue instanceof BooleanExpression) { + if ( + tryExpr instanceof BooleanExpression && + catchValue instanceof BooleanExpression + ) { return tryExpr.ifError(catchValue); } else { return tryExpr.ifError(valueToDefaultExpr(catchValue)); @@ -7079,6 +7092,25 @@ export function stringReverse(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).stringReverse(); } +/** + * Creates an expression that computes the absolute value of a numeric value. + * + * @param expr The expression to compute the absolute value of. + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ +export function abs(expr: Expression): FunctionExpression; + +/** + * Creates an expression that computes the absolute value of a numeric value. + * + * @param fieldName The field to compute the absolute value of. + * @return A new {@code Expr} representing the absolute value of the numeric value. + */ +export function abs(fieldName: string): FunctionExpression; +export function abs(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).abs(); +} + // TODO(new-expression): Add new top-level expression function definitions above this line /** diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 711582856c7..a6f96785399 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -45,7 +45,8 @@ import { log, sqrt, stringReverse, - length + length, + abs } from '../../../src/lite-api/expressions'; import { PipelineSnapshot } from '../../../src/lite-api/pipeline-result'; import { addEqualityMatcher } from '../../util/equality_matcher'; @@ -2583,9 +2584,12 @@ apiDescribe.only('Pipelines', persistence => { ifError(divide(constant(1), constant(0)), constant('was error')).as( 'ifError' ), - ifError(divide(constant(1), constant(0)).greaterThan(1), constant(true)).not().as( - 'ifErrorBooleanExpression' - ), + ifError( + divide(constant(1), constant(0)).greaterThan(1), + constant(true) + ) + .not() + .as('ifErrorBooleanExpression'), isAbsent('foo').as('isAbsent'), isNotNull('title').as('titleIsNotNull'), isNotNan('cost').as('costIsNotNan'), @@ -2619,9 +2623,11 @@ apiDescribe.only('Pipelines', persistence => { divide(constant(1), constant(0)) .ifError(constant('was error')) .as('ifError'), - divide(constant(1), constant(0)).greaterThan(1).ifError(constant(true)).not().as( - 'ifErrorBooleanExpression' - ), + divide(constant(1), constant(0)) + .greaterThan(1) + .ifError(constant(true)) + .not() + .as('ifErrorBooleanExpression'), field('foo').isAbsent().as('isAbsent'), field('title').isNotNull().as('titleIsNotNull'), field('cost').isNotNan().as('costIsNotNan') @@ -3765,6 +3771,30 @@ apiDescribe.only('Pipelines', persistence => { expectResults(snapshot, { reverseTitle: '4891' }); }); + it('testAbs', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(-10).as('neg10'), + constant(-22.22).as('neg22'), + constant(1).as('pos1') + ) + .select( + abs('neg10').as('10'), + abs(field('neg22')).as('22'), + field('pos1').as('1') + ) + ); + expectResults(snapshot, { + '10': 10, + '22': 22.22, + '1': 1 + }); + }); + // TODO(new-expression): Add new expression tests above this line }); diff --git a/packages/firestore/test/lite/pipeline.test.ts b/packages/firestore/test/lite/pipeline.test.ts index c227e556e23..c203eacd87f 100644 --- a/packages/firestore/test/lite/pipeline.test.ts +++ b/packages/firestore/test/lite/pipeline.test.ts @@ -99,7 +99,7 @@ import { arrayLength, charLength, divide, - byteLength, + abs, not, toLower, toUpper, @@ -1903,6 +1903,20 @@ describe('Firestore Pipelines', () => { }); }); + it('testAbs', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', 'To Kill a Mockingbird')) + .select(abs(field('rating')).as('absRating')) + .limit(1) + ); + expectResults(snapshot, { + absRating: 4.2 + }); + }); + it('testComparisonOperators', async () => { const snapshot = await execute( firestore From 6304b8e33e750c89f5ff2bfa89126aae02977d83 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:36:53 -0600 Subject: [PATCH 13/25] Implemente round with precision and removed Expr#log(base) due to potential for confusion --- .../firestore/src/lite-api/expressions.ts | 118 +++++++++++++----- .../test/integration/api/pipeline.test.ts | 28 ++++- packages/firestore/test/lite/pipeline.test.ts | 3 +- 3 files changed, 113 insertions(+), 36 deletions(-) diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 986138cb473..4e3046ea48c 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -1917,8 +1917,43 @@ export abstract class Expression implements ProtoValueSerializable, UserData { * * @return A new `Expr` representing the rounded value. */ - round(): FunctionExpression { - return new FunctionExpression('round', [this]); + round(): FunctionExpression; + /** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * field("price").round(2); + * ``` + * + * @param decimalPlaces A constant specifying the rounding precision in decimal places. + * + * @return A new `Expr` representing the rounded value. + */ + round(decimalPlaces: number): FunctionExpression; + /** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * field("price").round(constant(2)); + * ``` + * + * @param decimalPlaces An expression specifying the rounding precision in decimal places. + * + * @return A new `Expr` representing the rounded value. + */ + round(decimalPlaces: Expression): FunctionExpression; + round(decimalPlaces?: number | Expression): FunctionExpression { + if (decimalPlaces === undefined) { + return new FunctionExpression('round', [this]); + } else { + return new FunctionExpression( + 'round', + [this, valueToDefaultExpr(decimalPlaces)], + 'round' + ); + } } /** @@ -1966,35 +2001,6 @@ export abstract class Expression implements ProtoValueSerializable, UserData { return new FunctionExpression('ln', [this]); } - /** - * Creates an expression that computes the logarithm of this expression to a given base. - * - * ```typescript - * // Compute the logarithm of the 'value' field with base 10. - * field("value").log(10); - * ``` - * - * @param base The base of the logarithm. - * @return A new {@code Expr} representing the logarithm of the numeric value. - */ - log(base: number): FunctionExpression; - - /** - * Creates an expression that computes the logarithm of this expression to a given base. - * - * ```typescript - * // Compute the logarithm of the 'value' field with the base in the 'base' field. - * field("value").log(field("base")); - * ``` - * - * @param base The base of the logarithm. - * @return A new {@code Expr} representing the logarithm of the numeric value. - */ - log(base: Expression): FunctionExpression; - log(base: number | Expression): FunctionExpression { - return new FunctionExpression('log', [this, valueToDefaultExpr(base)]); - } - /** * Creates an expression that computes the square root of a numeric value. * @@ -6876,8 +6882,49 @@ export function round(fieldName: string): FunctionExpression; * @return A new `Expr` representing the rounded value. */ export function round(expression: Expression): FunctionExpression; -export function round(expr: Expression | string): FunctionExpression { - return fieldOrExpression(expr).round(); + +/** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * round("price", 2); + * ``` + * + * @param fieldName The name of the field to round. + * @param decimalPlaces A constant or expression specifying the rounding precision in decimal places. + * @return A new `Expr` representing the rounded value. + */ +export function round( + fieldName: string, + decimalPlaces: number | Expression +): FunctionExpression; + +/** + * Creates an expression that rounds a numeric value to the specified number of decimal places. + * + * ```typescript + * // Round the value of the 'price' field to two decimal places. + * round(field("price"), constant(2)); + * ``` + * + * @param expression An expression evaluating to a numeric value, which will be rounded. + * @param decimalPlaces A constant or expression specifying the rounding precision in decimal places. + * @return A new `Expr` representing the rounded value. + */ +export function round( + expression: Expression, + decimalPlaces: number | Expression +): FunctionExpression; +export function round( + expr: Expression | string, + decimalPlaces?: number | Expression +): FunctionExpression { + if (decimalPlaces === undefined) { + return fieldOrExpression(expr).round(); + } else { + return fieldOrExpression(expr).round(valueToDefaultExpr(decimalPlaces)); + } } /** @@ -7032,7 +7079,10 @@ export function log( expr: Expression | string, base: number | Expression ): FunctionExpression { - return fieldOrExpression(expr).log(valueToDefaultExpr(base)); + return new FunctionExpression('log', [ + fieldOrExpression(expr), + valueToDefaultExpr(base) + ]); } /** diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index a6f96785399..1531131b53b 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -3363,6 +3363,32 @@ apiDescribe.only('Pipelines', persistence => { }); }); + it('can round a numeric value to specified precision', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + foo: 4.123456 + }) + ) + .select( + field('foo').round(0).as('0'), + round('foo', 1).as('1'), + round('foo', constant(2)).as('2'), + round(field('foo'), 4).as('4') + ) + ); + expectResults(snapshot, { + '0': 4, + '1': 4.1, + '2': 4.12, + '4': 4.1235 + }); + }); + it('can get the collectionId from a path', async () => { const snapshot = await execute( firestore @@ -3532,7 +3558,7 @@ apiDescribe.only('Pipelines', persistence => { .collection(randomCol.path) .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(field('rating').log(10).as('logRating')) + .select(log(field('rating'), 10).as('logRating')) ); expectResults(snapshot, { logRating: 0.6232492903979004 diff --git a/packages/firestore/test/lite/pipeline.test.ts b/packages/firestore/test/lite/pipeline.test.ts index c203eacd87f..1633492b578 100644 --- a/packages/firestore/test/lite/pipeline.test.ts +++ b/packages/firestore/test/lite/pipeline.test.ts @@ -104,7 +104,8 @@ import { toLower, toUpper, trim, - arrayGet + arrayGet, + byteLength } from '../../src/lite-api/expressions'; import { documentId as documentIdFieldPath } from '../../src/lite-api/field_path'; import { vector } from '../../src/lite-api/field_value_impl'; From 9f70ee60e2a0ff8069b25cbacf65bbd8333c0df3 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:43:04 -0600 Subject: [PATCH 14/25] Add compile script to package.json for quicker development feedback --- packages/firestore/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 8d849c48d27..4834510608a 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -8,6 +8,7 @@ "author": "Firebase (https://firebase.google.com/)", "scripts": { "bundle": "rollup -c", + "compile": "tsc --emitDeclarationOnly --declaration -p tsconfig.json", "prebuild": "tsc --emitDeclarationOnly --declaration -p tsconfig.json; yarn api-report", "build": "run-p --npm-path npm build:lite build:main", "build:release": "yarn build && yarn typings:public", From e62f29b502ae1d0b69bd45652bfc99f059985a86 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:52:17 -0600 Subject: [PATCH 15/25] Rename esm2017 filenames to esm to match changes in package.json from main --- packages/firestore/rollup.config.js | 4 ++-- packages/firestore/rollup.config.lite.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/firestore/rollup.config.js b/packages/firestore/rollup.config.js index 1020e05be23..c9222f69ab4 100644 --- a/packages/firestore/rollup.config.js +++ b/packages/firestore/rollup.config.js @@ -178,8 +178,8 @@ const allBuilds = [ output: [ { dir: 'dist/', - entryFileNames: '[name].esm2017.js', - chunkFileNames: 'common-[hash].esm2017.js', + entryFileNames: '[name].esm.js', + chunkFileNames: 'common-[hash].esm.js', format: 'es', sourcemap: true } diff --git a/packages/firestore/rollup.config.lite.js b/packages/firestore/rollup.config.lite.js index 746502c7808..6bf9297e4d8 100644 --- a/packages/firestore/rollup.config.lite.js +++ b/packages/firestore/rollup.config.lite.js @@ -187,8 +187,8 @@ const allBuilds = [ output: [ { dir: 'dist/lite/', - entryFileNames: '[name].esm2017.js', - chunkFileNames: 'common-[hash].esm2017.js', + entryFileNames: '[name].esm.js', + chunkFileNames: 'common-[hash].esm.js', format: 'es', sourcemap: true } @@ -207,8 +207,8 @@ const allBuilds = [ input: ['./lite/index.ts', './lite/pipelines/pipelines.ts'], output: { dir: 'dist/lite/', - entryFileNames: '[name].rn.esm2017.js', - chunkFileNames: 'common-[hash].rn.esm2017.js', + entryFileNames: '[name].rn.esm.js', + chunkFileNames: 'common-[hash].rn.esm.js', format: 'es', sourcemap: true }, From 814e26862987c544b6e2ac8ce79eac5341c1aa71 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:46:46 -0600 Subject: [PATCH 16/25] Fixing circular dependency errors in the build. --- packages/firestore/src/api/snapshot.ts | 6 +- .../firestore/src/lite-api/expressions.ts | 45 ++++++++++++ .../firestore/src/lite-api/pipeline-result.ts | 7 +- .../firestore/src/lite-api/pipeline-source.ts | 9 ++- packages/firestore/src/lite-api/pipeline.ts | 27 ++++---- packages/firestore/src/lite-api/query.ts | 3 +- packages/firestore/src/lite-api/reference.ts | 6 ++ packages/firestore/src/lite-api/snapshot.ts | 24 +------ packages/firestore/src/util/types.ts | 68 ------------------- 9 files changed, 85 insertions(+), 110 deletions(-) diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index c82add0642a..86e075a4ca4 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -31,10 +31,12 @@ import { import { LiteUserDataWriter } from '../lite-api/reference_impl'; import { DocumentSnapshot as LiteDocumentSnapshot, - fieldPathFromArgument, FirestoreDataConverter as LiteFirestoreDataConverter } from '../lite-api/snapshot'; -import { UntypedFirestoreDataConverter } from '../lite-api/user_data_reader'; +import { + fieldPathFromArgument, + UntypedFirestoreDataConverter +} from '../lite-api/user_data_reader'; import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; import { fromBundledQuery } from '../local/local_serializer'; import { documentKeySet } from '../model/collections'; diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 4e3046ea48c..67f22b64b9b 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -7272,3 +7272,48 @@ export class Ordering implements ProtoValueSerializable, UserData { _protoValueType: 'ProtoValue' = 'ProtoValue'; } + +export function isSelectable(val: unknown): val is Selectable { + const candidate = val as Selectable; + return ( + candidate.selectable && isString(candidate.alias) && isExpr(candidate.expr) + ); +} + +export function isOrdering(val: unknown): val is Ordering { + const candidate = val as Ordering; + return ( + isExpr(candidate.expr) && + (candidate.direction === 'ascending' || + candidate.direction === 'descending') + ); +} + +export function isAliasedAggregate(val: unknown): val is AggregateWithAlias { + const candidate = val as AggregateWithAlias; + return ( + isString(candidate.alias) && + candidate.aggregate instanceof AggregateFunction + ); +} + +export function isExpr(val: unknown): val is Expression { + return val instanceof Expression; +} + +export function isBooleanExpr(val: unknown): val is BooleanExpression { + return val instanceof BooleanExpression; +} + +export function isField(val: unknown): val is Field { + return val instanceof Field; +} + +export function toField(value: string | Field): Field { + if (isString(value)) { + const result = field(value); + return result; + } else { + return value as Field; + } +} diff --git a/packages/firestore/src/lite-api/pipeline-result.ts b/packages/firestore/src/lite-api/pipeline-result.ts index 901af2949f9..c352ba48338 100644 --- a/packages/firestore/src/lite-api/pipeline-result.ts +++ b/packages/firestore/src/lite-api/pipeline-result.ts @@ -18,12 +18,12 @@ import { ObjectValue } from '../model/object_value'; import { isOptionalEqual } from '../util/misc'; -import { Field } from './expressions'; +import { Field, isField } 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 { fieldPathFromArgument } from './user_data_reader'; import { AbstractUserDataWriter } from './user_data_writer'; export class PipelineSnapshot { @@ -200,6 +200,9 @@ export class PipelineResult { if (this._fields === undefined) { return undefined; } + if (isField(fieldPath)) { + fieldPath = fieldPath.fieldName; + } const value = this._fields.field( fieldPathFromArgument('DocumentSnapshot.get', fieldPath) diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts index 40624ccfac4..3f4d62cb0be 100644 --- a/packages/firestore/src/lite-api/pipeline-source.ts +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -18,10 +18,15 @@ import { DatabaseId } from '../core/database_info'; import { toPipeline } from '../core/pipeline-util'; import { Code, FirestoreError } from '../util/error'; -import { isCollectionReference, isString } from '../util/types'; +import { isString } from '../util/types'; import { Pipeline } from './pipeline'; -import { CollectionReference, DocumentReference, Query } from './reference'; +import { + CollectionReference, + DocumentReference, + isCollectionReference, + Query +} from './reference'; import { CollectionGroupSource, CollectionSource, diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts index 51edd6c2dd4..6ddd7c4198a 100644 --- a/packages/firestore/src/lite-api/pipeline.ts +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -29,17 +29,7 @@ import { selectablesToMap, vectorToExpr } from '../util/pipeline_util'; -import { - isAliasedAggregate, - isBooleanExpr, - isExpr, - isField, - isLitePipeline, - isOrdering, - isSelectable, - isString, - toField -} from '../util/types'; +import { isString } from '../util/types'; import { Firestore } from './database'; import { @@ -53,7 +43,14 @@ import { field, Ordering, Selectable, - _field + _field, + isSelectable, + isField, + isBooleanExpr, + isAliasedAggregate, + toField, + isOrdering, + isExpr } from './expressions'; import { AddFields, @@ -1214,7 +1211,7 @@ export class Pipeline implements ProtoSerializable { // Process argument union(s) from method overloads let options: {}; let otherPipeline: Pipeline; - if (isLitePipeline(otherOrOptions)) { + if (isPipeline(otherOrOptions)) { options = {}; otherPipeline = otherOrOptions; } else { @@ -1437,3 +1434,7 @@ export class Pipeline implements ProtoSerializable { return new Pipeline(db, userDataReader, userDataWriter, stages); } } + +export function isPipeline(val: unknown): val is Pipeline { + return val instanceof Pipeline; +} diff --git a/packages/firestore/src/lite-api/query.ts b/packages/firestore/src/lite-api/query.ts index f0a357b828c..f019f0d0936 100644 --- a/packages/firestore/src/lite-api/query.ts +++ b/packages/firestore/src/lite-api/query.ts @@ -52,8 +52,9 @@ import { import { FieldPath } from './field_path'; import { DocumentData, DocumentReference, Query } from './reference'; -import { DocumentSnapshot, fieldPathFromArgument } from './snapshot'; +import { DocumentSnapshot } from './snapshot'; import { + fieldPathFromArgument, newUserDataReader, parseQueryValue, UserDataReader diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index e6c5fd7b056..43eedecf8b4 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -440,6 +440,12 @@ export class CollectionReference< } } +export function isCollectionReference( + val: unknown +): val is CollectionReference { + return val instanceof CollectionReference; +} + /** * Gets a `CollectionReference` instance that refers to the collection at * the specified absolute path. diff --git a/packages/firestore/src/lite-api/snapshot.ts b/packages/firestore/src/lite-api/snapshot.ts index 024d26dd22e..ba7b08cf9dd 100644 --- a/packages/firestore/src/lite-api/snapshot.ts +++ b/packages/firestore/src/lite-api/snapshot.ts @@ -15,15 +15,13 @@ * limitations under the License. */ -import { Compat, getModularInstance } from '@firebase/util'; +import { getModularInstance } from '@firebase/util'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; -import { FieldPath as InternalFieldPath } from '../model/path'; import { arrayEquals } from '../util/misc'; import { Firestore } from './database'; -import { Field } from './expressions'; import { FieldPath } from './field_path'; import { DocumentData, @@ -35,7 +33,7 @@ import { WithFieldValue } from './reference'; import { - fieldPathFromDotSeparatedString, + fieldPathFromArgument, UntypedFirestoreDataConverter } from './user_data_reader'; import { AbstractUserDataWriter } from './user_data_writer'; @@ -510,21 +508,3 @@ export function snapshotEqual( return false; } - -/** - * Helper that calls `fromDotSeparatedString()` but wraps any error thrown. - */ -export function fieldPathFromArgument( - methodName: string, - arg: string | FieldPath | Compat | 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/util/types.ts b/packages/firestore/src/util/types.ts index e1b2bfb86c2..5ed96cd023c 100644 --- a/packages/firestore/src/util/types.ts +++ b/packages/firestore/src/util/types.ts @@ -15,19 +15,6 @@ * limitations under the License. */ -import { CollectionReference } from '../api'; -import { - AggregateFunction, - AggregateWithAlias, - BooleanExpression, - Expression, - field, - Field, - Ordering, - Selectable -} from '../lite-api/expressions'; -import { Pipeline as LitePipeline } from '../lite-api/pipeline'; - /** Sentinel value that sorts before any Mutation Batch ID. */ export const BATCHID_UNKNOWN = -1; @@ -97,58 +84,3 @@ export type OneOf = { [P in Exclude]?: undefined; }; }[keyof T]; - -export function isSelectable(val: unknown): val is Selectable { - const candidate = val as Selectable; - return ( - candidate.selectable && isString(candidate.alias) && isExpr(candidate.expr) - ); -} - -export function isOrdering(val: unknown): val is Ordering { - const candidate = val as Ordering; - return ( - isExpr(candidate.expr) && - (candidate.direction === 'ascending' || - candidate.direction === 'descending') - ); -} - -export function isAliasedAggregate(val: unknown): val is AggregateWithAlias { - const candidate = val as AggregateWithAlias; - return ( - isString(candidate.alias) && - candidate.aggregate instanceof AggregateFunction - ); -} - -export function isExpr(val: unknown): val is Expression { - return val instanceof Expression; -} - -export function isBooleanExpr(val: unknown): val is BooleanExpression { - return val instanceof BooleanExpression; -} - -export function isField(val: unknown): val is Field { - return val instanceof Field; -} - -export function isLitePipeline(val: unknown): val is LitePipeline { - return val instanceof LitePipeline; -} - -export function isCollectionReference( - val: unknown -): val is CollectionReference { - return val instanceof CollectionReference; -} - -export function toField(value: string | Field): Field { - if (isString(value)) { - const result = field(value); - return result; - } else { - return value as Field; - } -} From 4300d65e406b3a208377637bdb23779a41a78926 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:19:30 -0600 Subject: [PATCH 17/25] fixing circular dependency in the build --- packages/firestore/src/lite-api/expressions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 67f22b64b9b..6005e5731e6 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { vector } from '../api'; import { ParseContext } from '../api/parse_context'; import { DOCUMENT_KEY_NAME, @@ -35,6 +34,7 @@ import { isString } from '../util/types'; import { Bytes } from './bytes'; import { documentId as documentIdFieldPath, FieldPath } from './field_path'; +import { vector } from './field_value_impl'; import { GeoPoint } from './geo_point'; import { DocumentReference } from './reference'; import { Timestamp } from './timestamp'; From b78e49c46d209d0af19dbdd1705916bde05b36d4 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:20:25 -0600 Subject: [PATCH 18/25] fixing bundler errors --- packages/firestore/src/lite-api/stage.ts | 6 +++--- .../firestore/src/platform/rn_lite/snapshot_to_json.ts | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/firestore/src/lite-api/stage.ts b/packages/firestore/src/lite-api/stage.ts index ac726ae2a47..5dd30eedba7 100644 --- a/packages/firestore/src/lite-api/stage.ts +++ b/packages/firestore/src/lite-api/stage.ts @@ -44,8 +44,6 @@ import { Pipeline } from './pipeline'; import { StageOptions } from './stage_options'; import { isUserData, UserData } from './user_data_reader'; -import Value = firestoreV1ApiClientInterfaces.Value; - /** * @beta */ @@ -56,7 +54,9 @@ export abstract class Stage implements ProtoSerializable, UserData { * @internal * @protected */ - protected optionsProto: ApiClientObjectMap | undefined = undefined; + protected optionsProto: + | ApiClientObjectMap + | undefined = undefined; protected knownOptions: Record; protected rawOptions?: Record; diff --git a/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts index 709509c8a4e..0ccebd779f5 100644 --- a/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts +++ b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts @@ -16,3 +16,10 @@ */ export { toByteStreamReader } from '../browser/byte_stream_reader'; + +// This is not included in the RN lite-bundle, but the rollup build +// will fail if these exports are not defined. +export { + buildDocumentSnapshotJsonBundle, + buildQuerySnapshotJsonBundle +} from '../browser/snapshot_to_json'; From 9695fa48f74585399766b01a97abf862e6a60138 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:35:32 -0600 Subject: [PATCH 19/25] Fix isNumber implementation --- packages/firestore/src/lite-api/pipeline.ts | 17 ++++++++++------- packages/firestore/src/util/types.ts | 4 ++++ .../test/integration/api/pipeline.test.ts | 5 +++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts index 6ddd7c4198a..0d542384000 100644 --- a/packages/firestore/src/lite-api/pipeline.ts +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -15,8 +15,6 @@ * limitations under the License. */ -import { isNumber } from 'util'; - import { Pipeline as ProtoPipeline, Stage as ProtoStage @@ -29,7 +27,7 @@ import { selectablesToMap, vectorToExpr } from '../util/pipeline_util'; -import { isString } from '../util/types'; +import { isNumber, isString } from '../util/types'; import { Firestore } from './database'; import { @@ -544,10 +542,15 @@ export class Pipeline implements ProtoSerializable { offset(options: OffsetStageOptions): Pipeline; offset(offsetOrOptions: number | OffsetStageOptions): Pipeline { // Process argument union(s) from method overloads - const options = isNumber(offsetOrOptions) ? {} : offsetOrOptions; - const offset: number = isNumber(offsetOrOptions) - ? offsetOrOptions - : offsetOrOptions.offset; + let options: {}; + let offset: number; + if (isNumber(offsetOrOptions)) { + options = {}; + offset = offsetOrOptions; + } else { + options = offsetOrOptions; + offset = offsetOrOptions.offset; + } // Create stage object const stage = new Offset(offset, options); diff --git a/packages/firestore/src/util/types.ts b/packages/firestore/src/util/types.ts index 5ed96cd023c..89ed50a240b 100644 --- a/packages/firestore/src/util/types.ts +++ b/packages/firestore/src/util/types.ts @@ -37,6 +37,10 @@ export function isNegativeZero(value: number): boolean { return value === 0 && 1 / value === 1 / -0; } +export function isNumber(value: unknown): value is number { + return typeof value === 'number'; +} + /** * Returns whether a value is an integer and in the safe integer range * @param value - The value to test for being an integer and in the safe range diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 1531131b53b..e8109b2a9ad 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -2823,8 +2823,7 @@ apiDescribe.only('Pipelines', persistence => { }); }); - // TODO re-enabled on fix of b/446938511 - it.skip('array contains any', async () => { + it('array contains any', async () => { const snapshot = await execute( firestore .pipeline() @@ -3984,6 +3983,7 @@ apiDescribe.only('Pipelines', persistence => { describe('stage options', () => { describe('forceIndex', () => { // SKIP: requires pre-existing index + // eslint-disable-next-line no-restricted-properties it.skip('Collection Stage', async () => { const snapshot = await execute( firestore.pipeline().collection({ @@ -3995,6 +3995,7 @@ apiDescribe.only('Pipelines', persistence => { }); // SKIP: requires pre-existing index + // eslint-disable-next-line no-restricted-properties it.skip('CollectionGroup Stage', async () => { const snapshot = await execute( firestore.pipeline().collectionGroup({ From e9afbe4badd7b6ddd531c51278f8ec2bf7283707 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:32:14 -0600 Subject: [PATCH 20/25] fix fishfood build --- packages/firestore/package.json | 6 +++++- packages/firestore/src/remote/datastore.ts | 13 ++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/firestore/package.json b/packages/firestore/package.json index da439112322..f3b9711359b 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -126,7 +126,11 @@ "license": "Apache-2.0", "files": [ "dist", - "lite/package.json" + "lite/package.json", + "pipelines/package.json", + "pipelines/pipelines.d.ts", + "lite/pipelines/package.json", + "lite/pipelines/pipelines.d.ts" ], "dependencies": { "@firebase/component": "0.7.0", diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index cae5b6a4507..8532cb9f000 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -262,17 +262,20 @@ export async function invokeExecutePipeline( executePipelineRequest ); - return response + const result: PipelineStreamElement[] = []; + response .filter(proto => !!proto.results) - .flatMap(proto => { + .forEach(proto => { if (proto.results!.length === 0) { - return fromPipelineResponse(datastoreImpl.serializer, proto); + result.push(fromPipelineResponse(datastoreImpl.serializer, proto)); } else { - return proto.results!.map(result => - fromPipelineResponse(datastoreImpl.serializer, proto, result) + return proto.results!.forEach(document => + result.push(fromPipelineResponse(datastoreImpl.serializer, proto, document)) ); } }); + + return result; } export async function invokeRunQueryRpc( From 78e7ae07cb9b63029f273e4990cd8e6f3af77a3c Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:27:41 -0600 Subject: [PATCH 21/25] Fixing exports --- packages/firestore/src/api_pipelines.ts | 19 +++++- .../test/integration/api/pipeline.test.ts | 58 +++++++++---------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index 65338462a60..e37186764cb 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -149,19 +149,32 @@ export { mapMerge, documentId, substring, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + length as len, + abs, Expression, AliasedExpression, Field, FunctionExpression, - Ordering + Ordering, + BooleanExpression, + AggregateFunction } from './lite-api/expressions'; export type { ExpressionType, AggregateWithAlias, Selectable, - BooleanExpression, - AggregateFunction } from './lite-api/expressions'; export { _internalPipelineToExecutePipelineRequestProto } from './remote/internal_serializer'; diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index e8109b2a9ad..3c4aa857e32 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -19,35 +19,6 @@ import { FirebaseError } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { - AggregateFunction, - arrayGet, - ascending, - BooleanExpression, - byteLength, - FunctionExpression, - timestampAdd, - timestampToUnixMicros, - timestampToUnixMillis, - timestampToUnixSeconds, - toLower, - unixMicrosToTimestamp, - unixMillisToTimestamp, - vectorLength, - countDistinct, - ceil, - floor, - exp, - pow, - round, - collectionId, - ln, - log, - sqrt, - stringReverse, - length, - abs -} from '../../../src/lite-api/expressions'; import { PipelineSnapshot } from '../../../src/lite-api/pipeline-result'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { Deferred } from '../../util/promise'; @@ -140,7 +111,34 @@ import { field, constant, _internalPipelineToExecutePipelineRequestProto, - FindNearestStageOptions + FindNearestStageOptions, + AggregateFunction, + arrayGet, + ascending, + BooleanExpression, + byteLength, + FunctionExpression, + timestampAdd, + timestampToUnixMicros, + timestampToUnixMillis, + timestampToUnixSeconds, + toLower, + unixMicrosToTimestamp, + unixMillisToTimestamp, + vectorLength, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + len as length, + abs } from '../util/pipeline_export'; use(chaiAsPromised); From 41d85069653c4306de45f07c80f8ede14f281689 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:44:58 -0600 Subject: [PATCH 22/25] Implement concat --- packages/firestore/src/api_pipelines.ts | 3 +- .../firestore/src/lite-api/expressions.ts | 71 +++++++++++++++++++ .../test/integration/api/pipeline.test.ts | 23 +++++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index e37186764cb..f96c826a5aa 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -162,6 +162,7 @@ export { stringReverse, length as len, abs, + concat, Expression, AliasedExpression, Field, @@ -174,7 +175,7 @@ export { export type { ExpressionType, AggregateWithAlias, - Selectable, + Selectable } from './lite-api/expressions'; export { _internalPipelineToExecutePipelineRequestProto } from './remote/internal_serializer'; diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 6005e5731e6..f4458791a81 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -1046,6 +1046,27 @@ export abstract class Expression implements ProtoValueSerializable, UserData { ); } + /** + * Creates an expression that concatenates expression results together. + * + * ```typescript + * // Combine the 'firstName', ' ', and 'lastName' fields into a single value. + * field("firstName").concat(constant(" "), field("lastName")); + * ``` + * + * @param second The additional expression or literal to concatenate. + * @param others Optional additional expressions or literals to concatenate. + * @return A new `Expr` representing the concatenated value. + */ + concat( + second: Expression | unknown, + ...others: Array + ): FunctionExpression { + const elements = [second, ...others]; + const exprs = elements.map(valueToDefaultExpr); + return new FunctionExpression('concat', [this, ...exprs], 'concat'); + } + /** * Creates an expression that reverses this string expression. * @@ -7142,6 +7163,56 @@ export function stringReverse(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).stringReverse(); } +/** + * Creates an expression that concatenates strings, arrays, or blobs. Types cannot be mixed. + * + * ```typescript + * // Concatenate the 'firstName' and 'lastName' fields with a space in between. + * concat(field("firstName"), " ", field("lastName")) + * ``` + * + * @param first The first expressions to concatenate. + * @param second The second literal or expression to concatenate. + * @param others Additional literals or expressions to concatenate. + * @return A new `Expression` representing the concatenation. + */ +export function concat( + first: Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +/** + * Creates an expression that concatenates strings, arrays, or blobs. Types cannot be mixed. + * + * ```typescript + * // Concatenate a field with a literal string. + * concat(field("firstName"), "Doe") + * ``` + * + * @param fieldName The name of a field to concatenate. + * @param second The second literal or expression to concatenate. + * @param others Additional literal or expressions to concatenate. + * @return A new `Expression` representing the concatenation. + */ +export function concat( + fieldName: string, + second: Expression | unknown, + ...others: Array +): FunctionExpression; + +export function concat( + fieldNameOrExpression: string | Expression, + second: Expression | unknown, + ...others: Array +): FunctionExpression { + return new FunctionExpression('concat', [ + fieldOrExpression(fieldNameOrExpression), + valueToDefaultExpr(second), + ...others.map(valueToDefaultExpr) + ]); +} + /** * Creates an expression that computes the absolute value of a numeric value. * diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 3c4aa857e32..44a30837e0e 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -138,7 +138,8 @@ import { sqrt, stringReverse, len as length, - abs + abs, + concat } from '../util/pipeline_export'; use(chaiAsPromised); @@ -988,6 +989,26 @@ apiDescribe.only('Pipelines', persistence => { }); }); + describe('concat stage', () => { + it('can concat fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + concat('author', ' ', field('title')).as('display'), + field('author').concat(': ', field('title')).as('display2') + ) + .where(equal('author', 'Douglas Adams')) + .select('display', 'display2') + ); + expectResults(snapshot, { + display: "Douglas Adams The Hitchhiker's Guide to the Galaxy", + display2: "Douglas Adams: The Hitchhiker's Guide to the Galaxy" + }); + }); + }); + describe('distinct stage', () => { it('returns distinct values as expected', async () => { const snapshot = await execute( From 5ddc9ce46db6be499dd195a30f4e1b2bffb33d4f Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:45:08 -0600 Subject: [PATCH 23/25] prettier --- packages/firestore/src/remote/datastore.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index 8532cb9f000..081b8cf5c9a 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -270,7 +270,9 @@ export async function invokeExecutePipeline( result.push(fromPipelineResponse(datastoreImpl.serializer, proto)); } else { return proto.results!.forEach(document => - result.push(fromPipelineResponse(datastoreImpl.serializer, proto, document)) + result.push( + fromPipelineResponse(datastoreImpl.serializer, proto, document) + ) ); } }); From 2cbd653c0c337c26f3f907ad6ffa0cd5af4d9e7b Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:43:50 -0600 Subject: [PATCH 24/25] error, ifabsent, currenttimestamp, join --- packages/firestore/src/api_pipelines.ts | 4 + .../firestore/src/lite-api/expressions.ts | 223 ++++++++++++++++++ .../test/integration/api/pipeline.test.ts | 116 +++++++-- 3 files changed, 322 insertions(+), 21 deletions(-) diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index f96c826a5aa..e7d221304b7 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -163,6 +163,10 @@ export { length as len, abs, concat, + currentTimestamp, + error, + ifAbsent, + join, Expression, AliasedExpression, Field, diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index f4458791a81..85b12f5dbd9 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -2050,6 +2050,71 @@ export abstract class Expression implements ProtoValueSerializable, UserData { return new FunctionExpression('string_reverse', [this]); } + /** + * Creates an expression that returns the `elseValue` argument if this expression results in an absent value, else + * return the result of the this expression evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * field("optional_field").ifAbsent("default_value") + * ``` + * + * @param elseValue The value that will be returned if this Expression evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ + ifAbsent(elseValue: unknown): Expression; + + /** + * Creates an expression that returns the `elseValue` argument if this expression results in an absent value, else + * return the result of this expression evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or if that is + * // absent, then returns the value of the field ` + * field("optional_field").ifAbsent(field('default_field')) + * ``` + * + * @param elseExpression The Expression that will be evaluated if this Expression evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ + ifAbsent(elseExpression: unknown): Expression; + + ifAbsent(elseValueOrExpression: Expression | unknown): Expression { + return new FunctionExpression('if_absent', [this, valueToDefaultExpr(elseValueOrExpression)], 'ifAbsent'); + } + + /** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with the delimiter from the 'separator' field. + * field("tags").join(field("separator")) + * ``` + * + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ + join(delimiterExpression: Expression): Expression; + + /** + * Creates an expression that joins the elements of an array field into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * field("tags").join(", ") + * ``` + * + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ + join(delimiter: string): Expression; + + join(delimeterValueOrExpression: string | Expression): Expression { + return new FunctionExpression('join', [this, valueToDefaultExpr(delimeterValueOrExpression)], 'join'); + } + + // TODO(new-expression): Add new expression method definitions above this line /** @@ -6768,6 +6833,37 @@ export function timestampSubtract( ); } +/** + * @beta + * + * Creates an expression that evaluates to the current server timestamp. + * + * ```typescript + * // Get the current server timestamp + * currentTimestamp() + * ``` + * + * @return A new Expression representing the current server timestamp. + */ +export function currentTimestamp(): FunctionExpression { + return new FunctionExpression('current_timestamp', [], 'currentTimestamp'); +} + +/** + * Creates an expression that raises an error with the given message. This could be useful for + * debugging purposes. + * + * ```typescript + * // Raise an error with the message "simulating an evaluation error". + * error("simulating an evaluation error") + * ``` + * + * @return A new Expression representing the error() operation. + */ +export function error(message: string): Expression { + return new FunctionExpression('error', [constant(message)], 'currentTimestamp'); +} + /** * @beta * @@ -7232,6 +7328,133 @@ export function abs(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).abs(); } +/** + * Creates an expression that returns the `elseExpr` argument if `ifExpr` is absent, else return + * the result of the `ifExpr` argument evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent(field("optional_field"), constant("default_value")) + * ``` + * + * @param ifExpr The expression to check for absence. + * @param elseExpr The expression that will be evaluated and returned if [ifExpr] is absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent(ifExpr: Expression, elseExpr: Expression): Expression; + +/** + * Creates an expression that returns the `elseValue` argument if `ifExpr` is absent, else + * return the result of the `ifExpr` argument evaluation. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent(field("optional_field"), "default_value") + * ``` + * + * @param ifExpr The expression to check for absence. + * @param elseValue The value that will be returned if `ifExpr` evaluates to an absent value. + * @return A new [Expression] representing the ifAbsent operation. + */ +export function ifAbsent(ifExpr: Expression, elseValue: unknown): Expression; + +/** + * Creates an expression that returns the `elseExpr` argument if `ifFieldName` is absent, else + * return the value of the field. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns the value of + * // 'default_field' if 'optional_field' is absent. + * ifAbsent("optional_field", field("default_field")) + * ``` + * + * @param ifFieldName The field to check for absence. + * @param elseExpr The expression that will be evaluated and returned if `ifFieldName` is + * absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent(ifFieldName: string, elseExpr: Expression): Expression; + +/** + * Creates an expression that returns the `elseValue` argument if `ifFieldName` is absent, else + * return the value of the field. + * + * ```typescript + * // Returns the value of the optional field 'optional_field', or returns 'default_value' + * // if the field is absent. + * ifAbsent("optional_field", "default_value") + * ``` + * + * @param ifFieldName The field to check for absence. + * @param elseValue The value that will be returned if [ifFieldName] is absent. + * @return A new Expression representing the ifAbsent operation. + */ +export function ifAbsent(ifFieldName: string | Expression, elseValue: Expression | unknown): Expression; +export function ifAbsent(fieldNameOrExpression: string | Expression, elseValue: Expression | unknown): Expression { + return fieldOrExpression(fieldNameOrExpression).ifAbsent(valueToDefaultExpr(elseValue)); +} + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * join("tags", ", ") + * ``` + * + * @param arrayFieldName The name of the field containing the array. + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ +export function join(arrayFieldName: string, delimiter: string): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join an array of string using the delimiter from the 'separator' field. + * join(array(['foo', 'bar']), field("separator")) + * ``` + * + * @param arrayExpression An expression that evaluates to an array. + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ +export function join(arrayExpression: Expression, delimiterExpression: Expression): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with a comma and space. + * join(field("tags"), ", ") + * ``` + * + * @param arrayExpression An expression that evaluates to an array. + * @param delimiter The string to use as a delimiter. + * @return A new Expression representing the join operation. + */ +export function join(arrayExpression: Expression, delimiter: string): Expression; + +/** + * Creates an expression that joins the elements of an array into a string. + * + * ```typescript + * // Join the elements of the 'tags' field with the delimiter from the 'separator' field. + * join('tags', field("separator")) + * ``` + * + * @param arrayFieldName The name of the field containing the array. + * @param delimiterExpression The expression that evaluates to the delimiter string. + * @return A new Expression representing the join operation. + */ +export function join(arrayFieldName: string, delimiterExpression: Expression): Expression; +export function join(fieldNameOrExpression: string | Expression, delimiterValueOrExpression: Expression | string): Expression { + return fieldOrExpression(fieldNameOrExpression).join(valueToDefaultExpr(delimiterValueOrExpression)); +} + // TODO(new-expression): Add new top-level expression function definitions above this line /** diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 44a30837e0e..0de26104238 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -139,7 +139,9 @@ import { stringReverse, len as length, abs, - concat + concat, + error, + currentTimestamp, ifAbsent, join } from '../util/pipeline_export'; use(chaiAsPromised); @@ -989,26 +991,6 @@ apiDescribe.only('Pipelines', persistence => { }); }); - describe('concat stage', () => { - it('can concat fields', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .addFields( - concat('author', ' ', field('title')).as('display'), - field('author').concat(': ', field('title')).as('display2') - ) - .where(equal('author', 'Douglas Adams')) - .select('display', 'display2') - ); - expectResults(snapshot, { - display: "Douglas Adams The Hitchhiker's Guide to the Galaxy", - display2: "Douglas Adams: The Hitchhiker's Guide to the Galaxy" - }); - }); - }); - describe('distinct stage', () => { it('returns distinct values as expected', async () => { const snapshot = await execute( @@ -3839,6 +3821,98 @@ apiDescribe.only('Pipelines', persistence => { }); }); + it('can concat fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + concat('author', ' ', field('title')).as('display'), + field('author').concat(': ', field('title')).as('display2') + ) + .where(equal('author', 'Douglas Adams')) + .select('display', 'display2') + ); + expectResults(snapshot, { + display: "Douglas Adams The Hitchhiker's Guide to the Galaxy", + display2: "Douglas Adams: The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('supports currentTimestamp', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .addFields( + currentTimestamp().as('now') + ) + .select('now') + ); + let now = snapshot.results[0].get('now') as Timestamp; + expect(now).instanceof(Timestamp); + expect(now.toDate().getUTCSeconds() - (new Date()).getUTCSeconds()).lessThan(5000); + }); + + it('supports error', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + isError(error('test error')).as('error') + )); + + expectResults(snapshot, { + 'error': true, + }); + }); + + it('supports ifAbsent', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith(map({ + title: 'foo' + })) + .select( + ifAbsent('title', 'default title').as('title'), + field('name').ifAbsent('default name').as('name'), + field('name').ifAbsent(field('title')).as('nameOrTitle'), + )); + + expectResults(snapshot, { + title: 'foo', + name: 'default name', + nameOrTitle: 'foo' + }); + }); + + it.only('supports join', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith(map({ + tags: ['foo', 'bar', 'baz'], + delimeter: '|' + })) + .select( + join('tags', ',').as('csv'), + field('tags').join('|').as('or') + )); + + expectResults(snapshot, { + csv: 'foo,bar,baz', + or: 'foo|bar|baz' + }); + }); + // TODO(new-expression): Add new expression tests above this line }); From c008c0646de995ffcbead892f5b91150d3e2e383 Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:16:29 -0600 Subject: [PATCH 25/25] arraySum and log10 --- packages/firestore/src/api_pipelines.ts | 2 + .../firestore/src/lite-api/expressions.ts | 141 ++++++++++++++++-- .../test/integration/api/pipeline.test.ts | 117 +++++++++++---- 3 files changed, 224 insertions(+), 36 deletions(-) diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index e7d221304b7..ee9850fe70e 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -167,6 +167,8 @@ export { error, ifAbsent, join, + log10, + arraySum, Expression, AliasedExpression, Field, diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index 85b12f5dbd9..df9c7a2e7de 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -2081,7 +2081,11 @@ export abstract class Expression implements ProtoValueSerializable, UserData { ifAbsent(elseExpression: unknown): Expression; ifAbsent(elseValueOrExpression: Expression | unknown): Expression { - return new FunctionExpression('if_absent', [this, valueToDefaultExpr(elseValueOrExpression)], 'ifAbsent'); + return new FunctionExpression( + 'if_absent', + [this, valueToDefaultExpr(elseValueOrExpression)], + 'ifAbsent' + ); } /** @@ -2111,9 +2115,40 @@ export abstract class Expression implements ProtoValueSerializable, UserData { join(delimiter: string): Expression; join(delimeterValueOrExpression: string | Expression): Expression { - return new FunctionExpression('join', [this, valueToDefaultExpr(delimeterValueOrExpression)], 'join'); + return new FunctionExpression( + 'join', + [this, valueToDefaultExpr(delimeterValueOrExpression)], + 'join' + ); } + /** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * field("value").log10(); + * ``` + * + * @return A new {@code Expr} representing the base-10 logarithm of the numeric value. + */ + log10(): FunctionExpression { + return new FunctionExpression('log10', [this]); + } + + /** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * field("scores").arraySum(); + * ``` + * + * @return A new {@code Expr} representing the sum of the elements in the array. + */ + arraySum(): FunctionExpression { + return new FunctionExpression('sum', [this]); + } // TODO(new-expression): Add new expression method definitions above this line @@ -6861,7 +6896,11 @@ export function currentTimestamp(): FunctionExpression { * @return A new Expression representing the error() operation. */ export function error(message: string): Expression { - return new FunctionExpression('error', [constant(message)], 'currentTimestamp'); + return new FunctionExpression( + 'error', + [constant(message)], + 'currentTimestamp' + ); } /** @@ -7391,9 +7430,17 @@ export function ifAbsent(ifFieldName: string, elseExpr: Expression): Expression; * @param elseValue The value that will be returned if [ifFieldName] is absent. * @return A new Expression representing the ifAbsent operation. */ -export function ifAbsent(ifFieldName: string | Expression, elseValue: Expression | unknown): Expression; -export function ifAbsent(fieldNameOrExpression: string | Expression, elseValue: Expression | unknown): Expression { - return fieldOrExpression(fieldNameOrExpression).ifAbsent(valueToDefaultExpr(elseValue)); +export function ifAbsent( + ifFieldName: string | Expression, + elseValue: Expression | unknown +): Expression; +export function ifAbsent( + fieldNameOrExpression: string | Expression, + elseValue: Expression | unknown +): Expression { + return fieldOrExpression(fieldNameOrExpression).ifAbsent( + valueToDefaultExpr(elseValue) + ); } /** @@ -7422,7 +7469,10 @@ export function join(arrayFieldName: string, delimiter: string): Expression; * @param delimiterExpression The expression that evaluates to the delimiter string. * @return A new Expression representing the join operation. */ -export function join(arrayExpression: Expression, delimiterExpression: Expression): Expression; +export function join( + arrayExpression: Expression, + delimiterExpression: Expression +): Expression; /** * Creates an expression that joins the elements of an array into a string. @@ -7436,7 +7486,10 @@ export function join(arrayExpression: Expression, delimiterExpression: Expressio * @param delimiter The string to use as a delimiter. * @return A new Expression representing the join operation. */ -export function join(arrayExpression: Expression, delimiter: string): Expression; +export function join( + arrayExpression: Expression, + delimiter: string +): Expression; /** * Creates an expression that joins the elements of an array into a string. @@ -7450,9 +7503,75 @@ export function join(arrayExpression: Expression, delimiter: string): Expression * @param delimiterExpression The expression that evaluates to the delimiter string. * @return A new Expression representing the join operation. */ -export function join(arrayFieldName: string, delimiterExpression: Expression): Expression; -export function join(fieldNameOrExpression: string | Expression, delimiterValueOrExpression: Expression | string): Expression { - return fieldOrExpression(fieldNameOrExpression).join(valueToDefaultExpr(delimiterValueOrExpression)); +export function join( + arrayFieldName: string, + delimiterExpression: Expression +): Expression; +export function join( + fieldNameOrExpression: string | Expression, + delimiterValueOrExpression: Expression | string +): Expression { + return fieldOrExpression(fieldNameOrExpression).join( + valueToDefaultExpr(delimiterValueOrExpression) + ); +} + +/** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * log10("value"); + * ``` + * + * @param fieldName The name of the field to compute the base-10 logarithm of. + * @return A new `Expr` representing the base-10 logarithm of the numeric value. + */ +export function log10(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the base-10 logarithm of a numeric value. + * + * ```typescript + * // Compute the base-10 logarithm of the 'value' field. + * log10(field("value")); + * ``` + * + * @param expression An expression evaluating to a numeric value, which the base-10 logarithm will be computed for. + * @return A new `Expr` representing the base-10 logarithm of the numeric value. + */ +export function log10(expression: Expression): FunctionExpression; +export function log10(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).log10(); +} + +/** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * arraySum("scores"); + * ``` + * + * @param fieldName The name of the field to compute the sum of. + * @return A new `Expr` representing the sum of the elements in the array. + */ +export function arraySum(fieldName: string): FunctionExpression; + +/** + * Creates an expression that computes the sum of the elements in an array. + * + * ```typescript + * // Compute the sum of the elements in the 'scores' field. + * arraySum(field("scores")); + * ``` + * + * @param expression An expression evaluating to a numeric array, which the sum will be computed for. + * @return A new `Expr` representing the sum of the elements in the array. + */ +export function arraySum(expression: Expression): FunctionExpression; +export function arraySum(expr: Expression | string): FunctionExpression { + return fieldOrExpression(expr).arraySum(); } // TODO(new-expression): Add new top-level expression function definitions above this line diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index 0de26104238..11d13dbb07f 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -141,7 +141,11 @@ import { abs, concat, error, - currentTimestamp, ifAbsent, join + currentTimestamp, + ifAbsent, + join, + log10, + arraySum } from '../util/pipeline_export'; use(chaiAsPromised); @@ -3821,6 +3825,36 @@ apiDescribe.only('Pipelines', persistence => { }); }); + it('can compute the base-10 logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(field('rating').log10().as('log10Rating')) + ); + expect(snapshot.results[0]!.data().log10Rating).to.be.closeTo( + 0.672, + 0.001 + ); + }); + + it('can compute the base-10 logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(log10('rating').as('log10Rating')) + ); + expect(snapshot.results[0]!.data().log10Rating).to.be.closeTo( + 0.672, + 0.001 + ); + }); + it('can concat fields', async () => { const snapshot = await execute( firestore @@ -3845,28 +3879,28 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .limit(1) - .addFields( - currentTimestamp().as('now') - ) + .addFields(currentTimestamp().as('now')) .select('now') ); - let now = snapshot.results[0].get('now') as Timestamp; + const now = snapshot.results[0].get('now') as Timestamp; expect(now).instanceof(Timestamp); - expect(now.toDate().getUTCSeconds() - (new Date()).getUTCSeconds()).lessThan(5000); + expect( + now.toDate().getUTCSeconds() - new Date().getUTCSeconds() + ).lessThan(5000); }); - it('supports error', async () => { + // Not implemented in backend + it.skip('supports error', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .limit(1) - .select( - isError(error('test error')).as('error') - )); + .select(isError(error('test error')).as('error')) + ); expectResults(snapshot, { - 'error': true, + 'error': true }); }); @@ -3876,14 +3910,17 @@ apiDescribe.only('Pipelines', persistence => { .pipeline() .collection(randomCol.path) .limit(1) - .replaceWith(map({ - title: 'foo' - })) + .replaceWith( + map({ + title: 'foo' + }) + ) .select( ifAbsent('title', 'default title').as('title'), field('name').ifAbsent('default name').as('name'), - field('name').ifAbsent(field('title')).as('nameOrTitle'), - )); + field('name').ifAbsent(field('title')).as('nameOrTitle') + ) + ); expectResults(snapshot, { title: 'foo', @@ -3892,20 +3929,20 @@ apiDescribe.only('Pipelines', persistence => { }); }); - it.only('supports join', async () => { + it('supports join', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .limit(1) - .replaceWith(map({ - tags: ['foo', 'bar', 'baz'], - delimeter: '|' - })) - .select( - join('tags', ',').as('csv'), - field('tags').join('|').as('or') - )); + .replaceWith( + map({ + tags: ['foo', 'bar', 'baz'], + delimeter: '|' + }) + ) + .select(join('tags', ',').as('csv'), field('tags').join('|').as('or')) + ); expectResults(snapshot, { csv: 'foo,bar,baz', @@ -3913,6 +3950,36 @@ apiDescribe.only('Pipelines', persistence => { }); }); + it('can compute the sum of the elements in an array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .addFields(array([150, 200]).as('sales')) + .select(field('sales').arraySum().as('totalSales')) + ); + expectResults(snapshot, { + totalSales: 350 + }); + }); + + it('can compute the sum of the elements in an array with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .addFields(array([150, 200]).as('sales')) + .select(arraySum('sales').as('totalSales')) + ); + expectResults(snapshot, { + totalSales: 350 + }); + }); + // TODO(new-expression): Add new expression tests above this line });