diff --git a/.changeset/spotty-shirts-design.md b/.changeset/spotty-shirts-design.md new file mode 100644 index 00000000000..77c6a8c8307 --- /dev/null +++ b/.changeset/spotty-shirts-design.md @@ -0,0 +1,5 @@ +--- +"@firebase/data-connect": patch +--- + +Fixed issue where onComplete wasn't triggering when the user calls `unsubscribe` on a subscription. diff --git a/common/api-review/ai.api.md b/common/api-review/ai.api.md index debea0a8549..08a12efc36e 100644 --- a/common/api-review/ai.api.md +++ b/common/api-review/ai.api.md @@ -92,6 +92,10 @@ export interface AudioConversationController { stop: () => Promise; } +// @public +export interface AudioTranscriptionConfig { +} + // @public export abstract class Backend { protected constructor(type: BackendType); @@ -153,6 +157,8 @@ export interface ChromeAdapter { generateContent(request: GenerateContentRequest): Promise; generateContentStream(request: GenerateContentRequest): Promise; isAvailable(request: GenerateContentRequest): Promise; + // @internal (undocumented) + mode: InferenceMode; } // @public @@ -256,6 +262,8 @@ export { Date_2 as Date } // @public export interface EnhancedGenerateContentResponse extends GenerateContentResponse { functionCalls: () => FunctionCall[] | undefined; + // @beta + inferenceSource?: InferenceSource; inlineDataParts: () => InlineDataPart[] | undefined; text: () => string; thoughtSummary: () => string | undefined; @@ -816,6 +824,15 @@ export const InferenceMode: { // @beta export type InferenceMode = (typeof InferenceMode)[keyof typeof InferenceMode]; +// @beta +export const InferenceSource: { + readonly ON_DEVICE: "on_device"; + readonly IN_CLOUD: "in_cloud"; +}; + +// @beta +export type InferenceSource = (typeof InferenceSource)[keyof typeof InferenceSource]; + // @public export interface InlineDataPart { // (undocumented) @@ -911,7 +928,9 @@ export interface LanguageModelPromptOptions { // @beta export interface LiveGenerationConfig { frequencyPenalty?: number; + inputAudioTranscription?: AudioTranscriptionConfig; maxOutputTokens?: number; + outputAudioTranscription?: AudioTranscriptionConfig; presencePenalty?: number; responseModalities?: ResponseModality[]; speechConfig?: SpeechConfig; @@ -964,8 +983,10 @@ export type LiveResponseType = (typeof LiveResponseType)[keyof typeof LiveRespon // @beta export interface LiveServerContent { + inputTranscription?: Transcription; interrupted?: boolean; modelTurn?: Content; + outputTranscription?: Transcription; turnComplete?: boolean; // (undocumented) type: 'serverContent'; @@ -994,9 +1015,14 @@ export class LiveSession { isClosed: boolean; receive(): AsyncGenerator; send(request: string | Array, turnComplete?: boolean): Promise; + sendAudioRealtime(blob: GenerativeContentBlob): Promise; sendFunctionResponses(functionResponses: FunctionResponse[]): Promise; + // @deprecated sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise; + // @deprecated (undocumented) sendMediaStream(mediaChunkStream: ReadableStream): Promise; + sendTextRealtime(text: string): Promise; + sendVideoRealtime(blob: GenerativeContentBlob): Promise; } // @public @@ -1326,6 +1352,11 @@ export interface ToolConfig { functionCallingConfig?: FunctionCallingConfig; } +// @beta +export interface Transcription { + text?: string; +} + // @public export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema | AnyOfSchema; diff --git a/common/api-review/auth.api.md b/common/api-review/auth.api.md index 0c9625a90e9..4253aa36dbd 100644 --- a/common/api-review/auth.api.md +++ b/common/api-review/auth.api.md @@ -195,6 +195,7 @@ export const AuthErrorCodes: { readonly MISSING_MFA_INFO: "auth/missing-multi-factor-info"; readonly MISSING_MFA_SESSION: "auth/missing-multi-factor-session"; readonly MISSING_PHONE_NUMBER: "auth/missing-phone-number"; + readonly MISSING_PASSWORD: "auth/missing-password"; readonly MISSING_SESSION_INFO: "auth/missing-verification-id"; readonly MODULE_DESTROYED: "auth/app-deleted"; readonly NEED_CONFIRMATION: "auth/account-exists-with-different-credential"; diff --git a/common/api-review/data-connect.api.md b/common/api-review/data-connect.api.md index 9e3d2424876..27a9d4af201 100644 --- a/common/api-review/data-connect.api.md +++ b/common/api-review/data-connect.api.md @@ -109,16 +109,6 @@ export interface DataConnectResult extends OpResult { ref: OperationRef; } -// @public -export interface DataConnectSubscription { - // (undocumented) - errCallback?: (e?: DataConnectError) => void; - // (undocumented) - unsubscribe: () => void; - // (undocumented) - userCallback: OnResultSubscription; -} - // @public (undocumented) export type DataSource = typeof SOURCE_CACHE | typeof SOURCE_SERVER; diff --git a/common/api-review/firestore-lite-pipelines.api.md b/common/api-review/firestore-lite-pipelines.api.md index 37882d2eb50..2fb04398a05 100644 --- a/common/api-review/firestore-lite-pipelines.api.md +++ b/common/api-review/firestore-lite-pipelines.api.md @@ -6,6 +6,12 @@ import { FirebaseApp } from '@firebase/app'; +// @beta +export function abs(expr: Expression): FunctionExpression; + +// @beta +export function abs(fieldName: string): FunctionExpression; + // @beta export function add(first: Expression, second: Expression | unknown): FunctionExpression; @@ -101,12 +107,30 @@ export function arrayContainsAny(array: Expression, values: Expression): Boolean // @beta export function arrayContainsAny(fieldName: string, values: Expression): BooleanExpression; +// @beta +export function arrayGet(arrayField: string, offset: number): FunctionExpression; + +// @beta +export function arrayGet(arrayField: string, offsetExpr: Expression): FunctionExpression; + +// @beta +export function arrayGet(arrayExpression: Expression, offset: number): FunctionExpression; + +// @beta +export function arrayGet(arrayExpression: Expression, offsetExpr: Expression): FunctionExpression; + // @beta export function arrayLength(fieldName: string): FunctionExpression; // @beta export function arrayLength(array: Expression): FunctionExpression; +// @beta +export function arraySum(fieldName: string): FunctionExpression; + +// @beta +export function arraySum(expression: Expression): FunctionExpression; + // @beta export function ascending(expr: Expression): Ordering; @@ -135,6 +159,12 @@ export function byteLength(expr: Expression): FunctionExpression; // @beta export function byteLength(fieldName: string): FunctionExpression; +// @beta +export function ceil(fieldName: string): FunctionExpression; + +// @beta +export function ceil(expression: Expression): FunctionExpression; + // @beta export function charLength(fieldName: string): FunctionExpression; @@ -147,68 +177,65 @@ export type CollectionGroupStageOptions = StageOptions & { forceIndex?: string; }; +// @beta +export function collectionId(fieldName: string): FunctionExpression; + +// @beta +export function collectionId(expression: Expression): FunctionExpression; + // @public export type CollectionStageOptions = StageOptions & { collection: string | Query; forceIndex?: string; }; +// @beta +export function concat(first: Expression, second: Expression | unknown, ...others: Array): FunctionExpression; + +// @beta +export function concat(fieldName: string, second: Expression | unknown, ...others: Array): FunctionExpression; + // @beta export function conditional(condition: BooleanExpression, thenExpr: Expression, elseExpr: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function constant(value: number): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function constant(value: string): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta -// -// @public +// @beta export function constant(value: boolean): BooleanExpression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function constant(value: null): Expression; // Warning: (ae-forgotten-export) The symbol "GeoPoint" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: GeoPoint): Expression; // Warning: (ae-forgotten-export) The symbol "Timestamp" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: Timestamp): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function constant(value: Date): Expression; // Warning: (ae-forgotten-export) The symbol "Bytes" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: Bytes): Expression; // Warning: (ae-forgotten-export) The symbol "DocumentReference" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: DocumentReference): Expression; // Warning: (ae-forgotten-export) The symbol "VectorValue" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: VectorValue): Expression; // @beta @@ -226,17 +253,21 @@ export function cosineDistance(vectorExpression: Expression, otherVectorExpressi // @beta export function count(expression: Expression): AggregateFunction; -// Warning: (ae-incompatible-release-tags) The symbol "count" is marked as @public, but its signature references "AggregateFunction" which is marked as @beta -// -// @public +// @beta export function count(fieldName: string): AggregateFunction; // @beta export function countAll(): AggregateFunction; +// @beta +export function countDistinct(expr: Expression | string): AggregateFunction; + // @beta export function countIf(booleanExpr: BooleanExpression): AggregateFunction; +// @beta +export function currentTimestamp(): FunctionExpression; + // @public export type DatabaseStageOptions = StageOptions & {}; @@ -334,9 +365,7 @@ export function euclideanDistance(vectorExpression: Expression, vector: number[] // @beta export function euclideanDistance(vectorExpression: Expression, otherVectorExpression: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "execute" is marked as @public, but its signature references "Pipeline" which is marked as @beta -// -// @public +// @beta export function execute(pipeline: Pipeline): Promise; // @beta @@ -345,6 +374,12 @@ export function exists(value: Expression): BooleanExpression; // @beta export function exists(fieldName: string): BooleanExpression; +// @beta +export function exp(expression: Expression): FunctionExpression; + +// @beta +export function exp(fieldName: string): FunctionExpression; + // @beta export abstract class Expression { abs(): FunctionExpression; @@ -456,14 +491,6 @@ export abstract class Expression { /* Excluded from this release type: _readUserData */ isError(): BooleanExpression; /* Excluded from this release type: _readUserData */ - isNan(): BooleanExpression; - /* Excluded from this release type: _readUserData */ - isNotNan(): BooleanExpression; - /* Excluded from this release type: _readUserData */ - isNotNull(): BooleanExpression; - /* Excluded from this release type: _readUserData */ - isNull(): BooleanExpression; - /* Excluded from this release type: _readUserData */ join(delimiterExpression: Expression): Expression; /* Excluded from this release type: _readUserData */ join(delimiter: string): Expression; @@ -536,6 +563,10 @@ export abstract class Expression { /* Excluded from this release type: _readUserData */ round(decimalPlaces: Expression): FunctionExpression; /* Excluded from this release type: _readUserData */ + split(delimiter: string): FunctionExpression; + /* Excluded from this release type: _readUserData */ + split(delimiter: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ sqrt(): FunctionExpression; /* Excluded from this release type: _readUserData */ startsWith(prefix: string): BooleanExpression; @@ -574,11 +605,17 @@ export abstract class Expression { /* Excluded from this release type: _readUserData */ timestampToUnixSeconds(): FunctionExpression; /* Excluded from this release type: _readUserData */ + timestampTruncate(granularity: TimeGranularity, timezone?: string | Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampTruncate(granularity: Expression, timezone?: string | Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ toLower(): FunctionExpression; /* Excluded from this release type: _readUserData */ toUpper(): FunctionExpression; /* Excluded from this release type: _readUserData */ - trim(): FunctionExpression; + trim(valueToTrim?: string | Expression | Bytes): FunctionExpression; + /* Excluded from this release type: _readUserData */ + type(): FunctionExpression; /* Excluded from this release type: _readUserData */ unixMicrosToTimestamp(): FunctionExpression; /* Excluded from this release type: _readUserData */ @@ -606,9 +643,7 @@ export class Field extends Expression implements Selectable { selectable: true; } -// Warning: (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta -// -// @public +// @beta export function field(name: string): Field; // Warning: (ae-forgotten-export) The symbol "FieldPath" needs to be exported by the entry point pipelines.d.ts @@ -626,6 +661,12 @@ export type FindNearestStageOptions = StageOptions & { distanceField?: string; }; +// @beta +export function floor(expr: Expression): FunctionExpression; + +// @beta +export function floor(fieldName: string): FunctionExpression; + // @beta export class FunctionExpression extends Expression { constructor(name: string, params: Expression[]); @@ -658,6 +699,18 @@ export function greaterThanOrEqual(fieldName: string, value: Expression): Boolea // @beta export function greaterThanOrEqual(fieldName: string, value: unknown): BooleanExpression; +// @beta +export function ifAbsent(ifExpr: Expression, elseExpr: Expression): Expression; + +// @beta +export function ifAbsent(ifExpr: Expression, elseValue: unknown): Expression; + +// @beta +export function ifAbsent(ifFieldName: string, elseExpr: Expression): Expression; + +// @beta +export function ifAbsent(ifFieldName: string | Expression, elseValue: Expression | unknown): Expression; + // @beta export function ifError(tryExpr: BooleanExpression, catchExpr: BooleanExpression): BooleanExpression; @@ -677,28 +730,24 @@ export function isAbsent(field: string): BooleanExpression; export function isError(value: Expression): BooleanExpression; // @beta -export function isNan(value: Expression): BooleanExpression; +export function join(arrayFieldName: string, delimiter: string): Expression; // @beta -export function isNan(fieldName: string): BooleanExpression; +export function join(arrayExpression: Expression, delimiterExpression: Expression): Expression; // @beta -export function isNotNan(value: Expression): BooleanExpression; +export function join(arrayExpression: Expression, delimiter: string): Expression; // @beta -export function isNotNan(value: string): BooleanExpression; +export function join(arrayFieldName: string, delimiterExpression: Expression): Expression; // @beta -export function isNotNull(value: Expression): BooleanExpression; +function length_2(fieldName: string): FunctionExpression; // @beta -export function isNotNull(value: string): BooleanExpression; +function length_2(expression: Expression): FunctionExpression; -// @beta -export function isNull(value: Expression): BooleanExpression; - -// @beta -export function isNull(value: string): BooleanExpression; +export { length_2 as length } // @beta export function lessThan(left: Expression, right: Expression): BooleanExpression; @@ -718,10 +767,7 @@ export function lessThanOrEqual(left: Expression, right: Expression): BooleanExp // @beta export function lessThanOrEqual(expression: Expression, value: unknown): BooleanExpression; -// Warning: (ae-incompatible-release-tags) The symbol "lessThanOrEqual" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "lessThanOrEqual" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta -// -// @public +// @beta export function lessThanOrEqual(fieldName: string, expression: Expression): BooleanExpression; // @beta @@ -744,6 +790,30 @@ export type LimitStageOptions = StageOptions & { limit: number; }; +// @beta +export function ln(fieldName: string): FunctionExpression; + +// @beta +export function ln(expression: Expression): FunctionExpression; + +// @beta +export function log(expression: Expression, base: number): FunctionExpression; + +// @beta +export function log(expression: Expression, base: Expression): FunctionExpression; + +// @beta +export function log(fieldName: string, base: number): FunctionExpression; + +// @beta +export function log(fieldName: string, base: Expression): FunctionExpression; + +// @beta +export function log10(fieldName: string): FunctionExpression; + +// @beta +export function log10(expression: Expression): FunctionExpression; + // @beta export function logicalMaximum(first: Expression, second: Expression | unknown, ...others: Array): FunctionExpression; @@ -943,13 +1013,10 @@ export class PipelineResult { get updateTime(): Timestamp | undefined; } -// @public (undocumented) +// @beta export class PipelineSnapshot { - // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "Pipeline" which is marked as @beta - // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "PipelineResult" which is marked as @beta constructor(pipeline: Pipeline, results: PipelineResult[], executionTime?: Timestamp); get executionTime(): Timestamp; - // Warning: (ae-incompatible-release-tags) The symbol "results" is marked as @public, but its signature references "PipelineResult" which is marked as @beta get results(): PipelineResult[]; } @@ -966,6 +1033,18 @@ export class PipelineSource { documents(options: DocumentsStageOptions): PipelineType; } +// @beta +export function pow(base: Expression, exponent: Expression): FunctionExpression; + +// @beta +export function pow(base: Expression, exponent: number): FunctionExpression; + +// @beta +export function pow(base: string, exponent: Expression): FunctionExpression; + +// @beta +export function pow(base: string, exponent: number): FunctionExpression; + // @beta export function regexContains(fieldName: string, pattern: string): BooleanExpression; @@ -1006,6 +1085,18 @@ export function reverse(stringExpression: Expression): FunctionExpression; // @beta export function reverse(field: string): FunctionExpression; +// @beta +export function round(fieldName: string): FunctionExpression; + +// @beta +export function round(expression: Expression): FunctionExpression; + +// @beta +export function round(fieldName: string, decimalPlaces: number | Expression): FunctionExpression; + +// @beta +export function round(expression: Expression, decimalPlaces: number | Expression): FunctionExpression; + // @public export type SampleStageOptions = StageOptions & OneOf<{ percentage: number; @@ -1028,6 +1119,24 @@ export type SortStageOptions = StageOptions & { orderings: Ordering[]; }; +// @beta +export function split(fieldName: string, delimiter: string): FunctionExpression; + +// @beta +export function split(fieldName: string, delimiter: Expression): FunctionExpression; + +// @beta +export function split(expression: Expression, delimiter: string): FunctionExpression; + +// @beta +export function split(expression: Expression, delimiter: Expression): FunctionExpression; + +// @beta +export function sqrt(expression: Expression): FunctionExpression; + +// @beta +export function sqrt(fieldName: string): FunctionExpression; + // @public export interface StageOptions { rawOptions?: { @@ -1065,6 +1174,12 @@ export function stringContains(stringExpression: Expression, substring: string): // @beta export function stringContains(stringExpression: Expression, substring: Expression): BooleanExpression; +// @beta +export function stringReverse(stringExpression: Expression): FunctionExpression; + +// @beta +export function stringReverse(field: string): FunctionExpression; + // @beta export function substring(field: string, position: number, length?: number): FunctionExpression; @@ -1089,6 +1204,15 @@ export function subtract(fieldName: string, expression: Expression): FunctionExp // @beta export function subtract(fieldName: string, value: unknown): FunctionExpression; +// @beta +export function sum(expression: Expression): AggregateFunction; + +// @beta +export function sum(fieldName: string): AggregateFunction; + +// @public (undocumented) +export type TimeGranularity = 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'week(monday)' | 'week(tuesday)' | 'week(wednesday)' | 'week(thursday)' | 'week(friday)' | 'week(saturday)' | 'week(sunday)' | 'isoWeek' | 'month' | 'quarter' | 'year' | 'isoYear'; + // @beta export function timestampAdd(timestamp: Expression, unit: Expression, amount: Expression): FunctionExpression; @@ -1125,6 +1249,30 @@ export function timestampToUnixSeconds(expr: Expression): FunctionExpression; // @beta export function timestampToUnixSeconds(fieldName: string): FunctionExpression; +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function timestampTruncate(fieldName: string, granularity: TimeGranularity, timezone?: string | Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function timestampTruncate(fieldName: string, granularity: Expression, timezone?: string | Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function timestampTruncate(timestampExpression: Expression, granularity: TimeGranularity, timezone?: string | Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function timestampTruncate(timestampExpression: Expression, granularity: Expression, timezone?: string | Expression): FunctionExpression; + // @beta export function toLower(fieldName: string): FunctionExpression; @@ -1138,10 +1286,16 @@ export function toUpper(fieldName: string): FunctionExpression; export function toUpper(stringExpression: Expression): FunctionExpression; // @beta -export function trim(fieldName: string): FunctionExpression; +export function trim(fieldName: string, valueToTrim?: string | Expression): FunctionExpression; + +// @beta +export function trim(stringExpression: Expression, valueToTrim?: string | Expression): FunctionExpression; + +// @beta +export function type(fieldName: string): FunctionExpression; // @beta -export function trim(stringExpression: Expression): FunctionExpression; +export function type(expression: Expression): FunctionExpression; // @public export type UnionStageOptions = StageOptions & { @@ -1189,19 +1343,19 @@ export function xor(first: BooleanExpression, second: BooleanExpression, ...addi // Warnings were encountered during analysis: // -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:55:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:92:5 - (ae-incompatible-release-tags) The symbol "accumulators" is marked as @public, but its signature references "AliasedAggregate" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:97:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:606:5 - (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point pipelines.d.ts -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:862:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:2871:5 - (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5095:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Field" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5105:5 - (ae-incompatible-release-tags) The symbol "map" is marked as @public, but its signature references "Expression" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5175:5 - (ae-incompatible-release-tags) The symbol "selections" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5185:5 - (ae-incompatible-release-tags) The symbol "orderings" is marked as @public, but its signature references "Ordering" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5734:5 - (ae-incompatible-release-tags) The symbol "other" is marked as @public, but its signature references "Pipeline" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5834:5 - (ae-incompatible-release-tags) The symbol "selectable" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5876:5 - (ae-incompatible-release-tags) The symbol "condition" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:71:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:110:5 - (ae-incompatible-release-tags) The symbol "accumulators" is marked as @public, but its signature references "AliasedAggregate" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:115:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:772:5 - (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point pipelines.d.ts +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:1090:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:3254:5 - (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5774:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Field" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5784:5 - (ae-incompatible-release-tags) The symbol "map" is marked as @public, but its signature references "Expression" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5908:5 - (ae-incompatible-release-tags) The symbol "selections" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:5918:5 - (ae-incompatible-release-tags) The symbol "orderings" is marked as @public, but its signature references "Ordering" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:6715:5 - (ae-incompatible-release-tags) The symbol "other" is marked as @public, but its signature references "Pipeline" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:6815:5 - (ae-incompatible-release-tags) The symbol "selectable" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/lite/pipelines.d.ts:6857:5 - (ae-incompatible-release-tags) The symbol "condition" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta // (No @packageDocumentation comment for this package) diff --git a/common/api-review/firestore-pipelines.api.md b/common/api-review/firestore-pipelines.api.md index add73ae322f..04f7d3214fb 100644 --- a/common/api-review/firestore-pipelines.api.md +++ b/common/api-review/firestore-pipelines.api.md @@ -6,15 +6,10 @@ import { FirebaseApp } from '@firebase/app'; -// Warning: (ae-incompatible-release-tags) The symbol "abs" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "abs" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function abs(expr: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "abs" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function abs(fieldName: string): FunctionExpression; // @beta @@ -23,21 +18,11 @@ export function add(first: Expression, second: Expression | unknown): FunctionEx // @beta export function add(fieldName: string, second: Expression | unknown): FunctionExpression; -// @beta (undocumented) -export class AddFields extends Stage { - constructor(fields: Map, options: StageOptions); - } - // @public export type AddFieldsStageOptions = StageOptions & { fields: Selectable[]; }; -// @beta (undocumented) -export class Aggregate extends Stage { - constructor(groups: Map, accumulators: Map, options: StageOptions); - } - // @beta export class AggregateFunction { constructor(name: string, params: Expression[]); @@ -140,15 +125,10 @@ export function arrayLength(fieldName: string): FunctionExpression; // @beta export function arrayLength(array: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "arraySum" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function arraySum(fieldName: string): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "arraySum" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "arraySum" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function arraySum(expression: Expression): FunctionExpression; // @beta @@ -179,15 +159,10 @@ export function byteLength(expr: Expression): FunctionExpression; // @beta export function byteLength(fieldName: string): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "ceil" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function ceil(fieldName: string): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "ceil" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "ceil" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function ceil(expression: Expression): FunctionExpression; // @beta @@ -196,107 +171,71 @@ export function charLength(fieldName: string): FunctionExpression; // @beta export function charLength(stringExpression: Expression): FunctionExpression; -// @beta (undocumented) -export class CollectionGroupSource extends Stage { - constructor(collectionId: string, options: StageOptions); - } - // @public export type CollectionGroupStageOptions = StageOptions & { collectionId: string; forceIndex?: string; }; -// Warning: (ae-incompatible-release-tags) The symbol "collectionId" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function collectionId(fieldName: string): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "collectionId" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "collectionId" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function collectionId(expression: Expression): FunctionExpression; -// @beta (undocumented) -export class CollectionSource extends Stage { - constructor(collection: string, options: StageOptions); - } - // @public export type CollectionStageOptions = StageOptions & { collection: string | Query; forceIndex?: string; }; -// Warning: (ae-incompatible-release-tags) The symbol "concat" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "concat" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function concat(first: Expression, second: Expression | unknown, ...others: Array): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "concat" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "concat" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function concat(fieldName: string, second: Expression | unknown, ...others: Array): FunctionExpression; // @beta export function conditional(condition: BooleanExpression, thenExpr: Expression, elseExpr: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function constant(value: number): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function constant(value: string): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta -// -// @public +// @beta export function constant(value: boolean): BooleanExpression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function constant(value: null): Expression; // Warning: (ae-forgotten-export) The symbol "GeoPoint" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: GeoPoint): Expression; // Warning: (ae-forgotten-export) The symbol "Timestamp" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: Timestamp): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function constant(value: Date): Expression; // Warning: (ae-forgotten-export) The symbol "Bytes" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: Bytes): Expression; // Warning: (ae-forgotten-export) The symbol "DocumentReference" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: DocumentReference): Expression; // Warning: (ae-forgotten-export) The symbol "VectorValue" needs to be exported by the entry point pipelines.d.ts -// Warning: (ae-incompatible-release-tags) The symbol "constant" is marked as @public, but its signature references "Expression" which is marked as @beta // -// @public +// @beta export function constant(value: VectorValue): Expression; // @beta @@ -314,18 +253,13 @@ export function cosineDistance(vectorExpression: Expression, otherVectorExpressi // @beta export function count(expression: Expression): AggregateFunction; -// Warning: (ae-incompatible-release-tags) The symbol "count" is marked as @public, but its signature references "AggregateFunction" which is marked as @beta -// -// @public +// @beta export function count(fieldName: string): AggregateFunction; // @beta export function countAll(): AggregateFunction; -// Warning: (ae-incompatible-release-tags) The symbol "countDistinct" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "countDistinct" is marked as @public, but its signature references "AggregateFunction" which is marked as @beta -// -// @public +// @beta export function countDistinct(expr: Expression | string): AggregateFunction; // @beta @@ -334,10 +268,6 @@ export function countIf(booleanExpr: BooleanExpression): AggregateFunction; // @beta export function currentTimestamp(): FunctionExpression; -// @beta (undocumented) -export class DatabaseSource extends Stage { -} - // @public export type DatabaseStageOptions = StageOptions & {}; @@ -347,11 +277,6 @@ export function descending(expr: Expression): Ordering; // @beta export function descending(fieldName: string): Ordering; -// @beta (undocumented) -export class Distinct extends Stage { - constructor(groups: Map, options: StageOptions); - } - // @public export type DistinctStageOptions = StageOptions & { groups: Array; @@ -375,11 +300,6 @@ export function documentId(documentPath: string | DocumentReference): FunctionEx // @beta export function documentId(documentPathExpr: Expression): FunctionExpression; -// @beta (undocumented) -export class DocumentsSource extends Stage { - constructor(docPaths: string[], options: StageOptions); - } - // @public export type DocumentsStageOptions = StageOptions & { docs: Array; @@ -433,11 +353,6 @@ export function equalAny(fieldName: string, values: Array) // @beta export function equalAny(fieldName: string, arrayExpression: Expression): BooleanExpression; -// Warning: (ae-incompatible-release-tags) The symbol "error" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public -export function error(message: string): Expression; - // @beta export function euclideanDistance(fieldName: string, vector: number[] | VectorValue): FunctionExpression; @@ -450,9 +365,11 @@ export function euclideanDistance(vectorExpression: Expression, vector: number[] // @beta export function euclideanDistance(vectorExpression: Expression, otherVectorExpression: Expression): FunctionExpression; -// @public +// @beta export function execute(pipeline: Pipeline): Promise; +// Warning: (ae-incompatible-release-tags) The symbol "execute" is marked as @public, but its signature references "PipelineSnapshot" which is marked as @beta +// // @public (undocumented) export function execute(options: PipelineExecuteOptions): Promise; @@ -462,15 +379,10 @@ export function exists(value: Expression): BooleanExpression; // @beta export function exists(fieldName: string): BooleanExpression; -// Warning: (ae-incompatible-release-tags) The symbol "exp" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "exp" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function exp(expression: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "exp" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function exp(fieldName: string): FunctionExpression; // @beta @@ -584,14 +496,6 @@ export abstract class Expression { /* Excluded from this release type: _readUserData */ isError(): BooleanExpression; /* Excluded from this release type: _readUserData */ - isNan(): BooleanExpression; - /* Excluded from this release type: _readUserData */ - isNotNan(): BooleanExpression; - /* Excluded from this release type: _readUserData */ - isNotNull(): BooleanExpression; - /* Excluded from this release type: _readUserData */ - isNull(): BooleanExpression; - /* Excluded from this release type: _readUserData */ join(delimiterExpression: Expression): Expression; /* Excluded from this release type: _readUserData */ join(delimiter: string): Expression; @@ -664,6 +568,10 @@ export abstract class Expression { /* Excluded from this release type: _readUserData */ round(decimalPlaces: Expression): FunctionExpression; /* Excluded from this release type: _readUserData */ + split(delimiter: string): FunctionExpression; + /* Excluded from this release type: _readUserData */ + split(delimiter: Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ sqrt(): FunctionExpression; /* Excluded from this release type: _readUserData */ startsWith(prefix: string): BooleanExpression; @@ -702,11 +610,17 @@ export abstract class Expression { /* Excluded from this release type: _readUserData */ timestampToUnixSeconds(): FunctionExpression; /* Excluded from this release type: _readUserData */ + timestampTruncate(granularity: TimeGranularity, timezone?: string | Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ + timestampTruncate(granularity: Expression, timezone?: string | Expression): FunctionExpression; + /* Excluded from this release type: _readUserData */ toLower(): FunctionExpression; /* Excluded from this release type: _readUserData */ toUpper(): FunctionExpression; /* Excluded from this release type: _readUserData */ - trim(): FunctionExpression; + trim(valueToTrim?: string | Expression | Bytes): FunctionExpression; + /* Excluded from this release type: _readUserData */ + type(): FunctionExpression; /* Excluded from this release type: _readUserData */ unixMicrosToTimestamp(): FunctionExpression; /* Excluded from this release type: _readUserData */ @@ -734,9 +648,7 @@ export class Field extends Expression implements Selectable { selectable: true; } -// Warning: (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta -// -// @public +// @beta export function field(name: string): Field; // Warning: (ae-forgotten-export) The symbol "FieldPath" needs to be exported by the entry point pipelines.d.ts @@ -745,11 +657,6 @@ export function field(name: string): Field; // @public (undocumented) export function field(path: FieldPath): Field; -// @beta (undocumented) -export class FindNearest extends Stage { - constructor(vectorValue: Expression, field: Field, distanceMeasure: 'euclidean' | 'cosine' | 'dot_product', options: StageOptions); - } - // @public export type FindNearestStageOptions = StageOptions & { field: Field | string; @@ -759,15 +666,10 @@ export type FindNearestStageOptions = StageOptions & { distanceField?: string; }; -// Warning: (ae-incompatible-release-tags) The symbol "floor" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "floor" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function floor(expr: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "floor" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function floor(fieldName: string): FunctionExpression; // @beta @@ -802,24 +704,16 @@ export function greaterThanOrEqual(fieldName: string, value: Expression): Boolea // @beta export function greaterThanOrEqual(fieldName: string, value: unknown): BooleanExpression; -// Warning: (ae-incompatible-release-tags) The symbol "ifAbsent" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function ifAbsent(ifExpr: Expression, elseExpr: Expression): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "ifAbsent" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function ifAbsent(ifExpr: Expression, elseValue: unknown): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "ifAbsent" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function ifAbsent(ifFieldName: string, elseExpr: Expression): Expression; -// Warning: (ae-incompatible-release-tags) The symbol "ifAbsent" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public +// @beta export function ifAbsent(ifFieldName: string | Expression, elseValue: Expression | unknown): Expression; // @beta @@ -841,59 +735,24 @@ export function isAbsent(field: string): BooleanExpression; export function isError(value: Expression): BooleanExpression; // @beta -export function isNan(value: Expression): BooleanExpression; +export function join(arrayFieldName: string, delimiter: string): Expression; // @beta -export function isNan(fieldName: string): BooleanExpression; +export function join(arrayExpression: Expression, delimiterExpression: Expression): Expression; // @beta -export function isNotNan(value: Expression): BooleanExpression; +export function join(arrayExpression: Expression, delimiter: string): Expression; // @beta -export function isNotNan(value: string): BooleanExpression; +export function join(arrayFieldName: string, delimiterExpression: Expression): Expression; // @beta -export function isNotNull(value: Expression): BooleanExpression; +function length_2(fieldName: string): FunctionExpression; // @beta -export function isNotNull(value: string): BooleanExpression; +function length_2(expression: Expression): FunctionExpression; -// @beta -export function isNull(value: Expression): BooleanExpression; - -// @beta -export function isNull(value: string): BooleanExpression; - -// Warning: (ae-incompatible-release-tags) The symbol "join" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public -export function join(arrayFieldName: string, delimiter: string): Expression; - -// Warning: (ae-incompatible-release-tags) The symbol "join" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public -export function join(arrayExpression: Expression, delimiterExpression: Expression): Expression; - -// Warning: (ae-incompatible-release-tags) The symbol "join" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public -export function join(arrayExpression: Expression, delimiter: string): Expression; - -// Warning: (ae-incompatible-release-tags) The symbol "join" is marked as @public, but its signature references "Expression" which is marked as @beta -// -// @public -export function join(arrayFieldName: string, delimiterExpression: Expression): Expression; - -// Warning: (ae-incompatible-release-tags) The symbol "len" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public -export function len(fieldName: string): FunctionExpression; - -// Warning: (ae-incompatible-release-tags) The symbol "len" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "len" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public -export function len(expression: Expression): FunctionExpression; +export { length_2 as length } // @beta export function lessThan(left: Expression, right: Expression): BooleanExpression; @@ -913,10 +772,7 @@ export function lessThanOrEqual(left: Expression, right: Expression): BooleanExp // @beta export function lessThanOrEqual(expression: Expression, value: unknown): BooleanExpression; -// Warning: (ae-incompatible-release-tags) The symbol "lessThanOrEqual" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "lessThanOrEqual" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta -// -// @public +// @beta export function lessThanOrEqual(fieldName: string, expression: Expression): BooleanExpression; // @beta @@ -934,59 +790,33 @@ export function like(stringExpression: Expression, pattern: string): BooleanExpr // @beta export function like(stringExpression: Expression, pattern: Expression): BooleanExpression; -// @beta (undocumented) -export class Limit extends Stage { - constructor(limit: number, options: StageOptions); - } - // @public export type LimitStageOptions = StageOptions & { limit: number; }; -// Warning: (ae-incompatible-release-tags) The symbol "ln" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function ln(fieldName: string): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "ln" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "ln" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function ln(expression: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function log(expression: Expression, base: number): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function log(expression: Expression, base: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function log(fieldName: string, base: number): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "log" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function log(fieldName: string, base: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "log10" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function log10(fieldName: string): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "log10" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "log10" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function log10(expression: Expression): FunctionExpression; // @beta @@ -1085,11 +915,6 @@ export function notEqualAny(element: Expression, arrayExpression: Expression): B // @beta export function notEqualAny(fieldName: string, arrayExpression: Expression): BooleanExpression; -// @beta (undocumented) -export class Offset extends Stage { - constructor(offset: number, options: StageOptions); - } - // @public export type OffsetStageOptions = StageOptions & { offset: number; @@ -1117,55 +942,82 @@ export class Ordering { // @public (undocumented) export class Pipeline { // Warning: (ae-incompatible-release-tags) The symbol "addFields" is marked as @public, but its signature references "Selectable" which is marked as @beta + // + // (undocumented) addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline; // (undocumented) addFields(options: AddFieldsStageOptions): Pipeline; // Warning: (ae-incompatible-release-tags) The symbol "aggregate" is marked as @public, but its signature references "AliasedAggregate" which is marked as @beta + // + // (undocumented) aggregate(accumulator: AliasedAggregate, ...additionalAccumulators: AliasedAggregate[]): Pipeline; + // (undocumented) aggregate(options: AggregateStageOptions): Pipeline; // Warning: (ae-incompatible-release-tags) The symbol "distinct" is marked as @public, but its signature references "Selectable" which is marked as @beta + // + // (undocumented) distinct(group: string | Selectable, ...additionalGroups: Array): Pipeline; // (undocumented) distinct(options: DistinctStageOptions): Pipeline; + // (undocumented) findNearest(options: FindNearestStageOptions): Pipeline; + // (undocumented) limit(limit: number): Pipeline; // (undocumented) limit(options: LimitStageOptions): Pipeline; + // (undocumented) offset(offset: number): Pipeline; // (undocumented) offset(options: OffsetStageOptions): Pipeline; + // (undocumented) rawStage(name: string, params: unknown[], options?: { [key: string]: Expression | unknown; }): Pipeline; // Warning: (ae-incompatible-release-tags) The symbol "removeFields" is marked as @public, but its signature references "Field" which is marked as @beta + // + // (undocumented) removeFields(fieldValue: Field | string, ...additionalFields: Array): Pipeline; // (undocumented) removeFields(options: RemoveFieldsStageOptions): Pipeline; + // (undocumented) replaceWith(fieldName: string): Pipeline; // Warning: (ae-incompatible-release-tags) The symbol "replaceWith" is marked as @public, but its signature references "Expression" which is marked as @beta + // + // (undocumented) replaceWith(expr: Expression): Pipeline; // (undocumented) replaceWith(options: ReplaceWithStageOptions): Pipeline; + // (undocumented) sample(documents: number): Pipeline; + // (undocumented) sample(options: SampleStageOptions): Pipeline; // Warning: (ae-incompatible-release-tags) The symbol "select" is marked as @public, but its signature references "Selectable" which is marked as @beta + // + // (undocumented) select(selection: Selectable | string, ...additionalSelections: Array): Pipeline; // (undocumented) select(options: SelectStageOptions): Pipeline; // Warning: (ae-incompatible-release-tags) The symbol "sort" is marked as @public, but its signature references "Ordering" which is marked as @beta + // + // (undocumented) sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline; // (undocumented) sort(options: SortStageOptions): Pipeline; // (undocumented) stages: any; + // (undocumented) union(other: Pipeline): Pipeline; // (undocumented) union(options: UnionStageOptions): Pipeline; // Warning: (ae-incompatible-release-tags) The symbol "unnest" is marked as @public, but its signature references "Selectable" which is marked as @beta + // + // (undocumented) unnest(selectable: Selectable, indexField?: string): Pipeline; // (undocumented) unnest(options: UnnestStageOptions): Pipeline; // (undocumented) userDataReader: any; // Warning: (ae-incompatible-release-tags) The symbol "where" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta + // + // (undocumented) where(condition: BooleanExpression): Pipeline; // (undocumented) where(options: WhereStageOptions): Pipeline; @@ -1200,12 +1052,10 @@ export class PipelineResult { // @public (undocumented) export function pipelineResultEqual(left: PipelineResult, right: PipelineResult): boolean; -// @public (undocumented) +// @beta export class PipelineSnapshot { - // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "PipelineResult" which is marked as @beta constructor(pipeline: Pipeline, results: PipelineResult[], executionTime?: Timestamp); get executionTime(): Timestamp; - // Warning: (ae-incompatible-release-tags) The symbol "results" is marked as @public, but its signature references "PipelineResult" which is marked as @beta get results(): PipelineResult[]; } @@ -1222,33 +1072,18 @@ export class PipelineSource { documents(options: DocumentsStageOptions): PipelineType; } -// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function pow(base: Expression, exponent: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function pow(base: Expression, exponent: number): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function pow(base: string, exponent: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "pow" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function pow(base: string, exponent: number): FunctionExpression; -// @beta (undocumented) -export class RawStage extends Stage { - } - // @beta export function regexContains(fieldName: string, pattern: string): BooleanExpression; @@ -1289,27 +1124,16 @@ export function reverse(stringExpression: Expression): FunctionExpression; // @beta export function reverse(field: string): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function round(fieldName: string): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function round(expression: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function round(fieldName: string, decimalPlaces: number | Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "round" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function round(expression: Expression, decimalPlaces: number | Expression): FunctionExpression; // @public @@ -1318,11 +1142,6 @@ export type SampleStageOptions = StageOptions & OneOf<{ documents: number; }>; -// @beta (undocumented) -export class Select extends Stage { - constructor(selections: Map, options: StageOptions); - } - // @beta export interface Selectable { // (undocumented) @@ -1334,37 +1153,29 @@ export type SelectStageOptions = StageOptions & { selections: Array; }; -// @beta (undocumented) -export class Sort extends Stage { - constructor(orderings: Ordering[], options: StageOptions); - } - // @public export type SortStageOptions = StageOptions & { orderings: Ordering[]; }; -// Warning: (ae-incompatible-release-tags) The symbol "sqrt" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "sqrt" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta +export function split(fieldName: string, delimiter: string): FunctionExpression; + +// @beta +export function split(fieldName: string, delimiter: Expression): FunctionExpression; + +// @beta +export function split(expression: Expression, delimiter: string): FunctionExpression; + +// @beta +export function split(expression: Expression, delimiter: Expression): FunctionExpression; + +// @beta export function sqrt(expression: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "sqrt" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function sqrt(fieldName: string): FunctionExpression; -// @beta (undocumented) -export abstract class Stage { - /* Excluded from this release type: optionsProto */ - constructor(options: StageOptions); - // (undocumented) - protected knownOptions: Record; - // (undocumented) - protected rawOptions?: Record; -} - // @public export interface StageOptions { rawOptions?: { @@ -1402,15 +1213,10 @@ export function stringContains(stringExpression: Expression, substring: string): // @beta export function stringContains(stringExpression: Expression, substring: Expression): BooleanExpression; -// Warning: (ae-incompatible-release-tags) The symbol "stringReverse" is marked as @public, but its signature references "Expression" which is marked as @beta -// Warning: (ae-incompatible-release-tags) The symbol "stringReverse" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function stringReverse(stringExpression: Expression): FunctionExpression; -// Warning: (ae-incompatible-release-tags) The symbol "stringReverse" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta -// -// @public +// @beta export function stringReverse(field: string): FunctionExpression; // @beta @@ -1443,6 +1249,9 @@ export function sum(expression: Expression): AggregateFunction; // @beta export function sum(fieldName: string): AggregateFunction; +// @public (undocumented) +export type TimeGranularity = 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'week(monday)' | 'week(tuesday)' | 'week(wednesday)' | 'week(thursday)' | 'week(friday)' | 'week(saturday)' | 'week(sunday)' | 'isoWeek' | 'month' | 'quarter' | 'year' | 'isoYear'; + // @beta export function timestampAdd(timestamp: Expression, unit: Expression, amount: Expression): FunctionExpression; @@ -1479,6 +1288,30 @@ export function timestampToUnixSeconds(expr: Expression): FunctionExpression; // @beta export function timestampToUnixSeconds(fieldName: string): FunctionExpression; +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function timestampTruncate(fieldName: string, granularity: TimeGranularity, timezone?: string | Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function timestampTruncate(fieldName: string, granularity: Expression, timezone?: string | Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function timestampTruncate(timestampExpression: Expression, granularity: TimeGranularity, timezone?: string | Expression): FunctionExpression; + +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "Expression" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "timestampTruncate" is marked as @public, but its signature references "FunctionExpression" which is marked as @beta +// +// @public +export function timestampTruncate(timestampExpression: Expression, granularity: Expression, timezone?: string | Expression): FunctionExpression; + // @beta export function toLower(fieldName: string): FunctionExpression; @@ -1492,10 +1325,16 @@ export function toUpper(fieldName: string): FunctionExpression; export function toUpper(stringExpression: Expression): FunctionExpression; // @beta -export function trim(fieldName: string): FunctionExpression; +export function trim(fieldName: string, valueToTrim?: string | Expression): FunctionExpression; // @beta -export function trim(stringExpression: Expression): FunctionExpression; +export function trim(stringExpression: Expression, valueToTrim?: string | Expression): FunctionExpression; + +// @beta +export function type(fieldName: string): FunctionExpression; + +// @beta +export function type(expression: Expression): FunctionExpression; // @public export type UnionStageOptions = StageOptions & { @@ -1532,11 +1371,6 @@ export function vectorLength(vectorExpression: Expression): FunctionExpression; // @beta export function vectorLength(fieldName: string): FunctionExpression; -// @beta (undocumented) -export class Where extends Stage { - constructor(condition: BooleanExpression, options: StageOptions); - } - // @public export type WhereStageOptions = StageOptions & { condition: BooleanExpression; @@ -1548,19 +1382,19 @@ export function xor(first: BooleanExpression, second: BooleanExpression, ...addi // Warnings were encountered during analysis: // -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:73:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:118:5 - (ae-incompatible-release-tags) The symbol "accumulators" is marked as @public, but its signature references "AliasedAggregate" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:123:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:785:5 - (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point pipelines.d.ts -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:1100:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:3160:5 - (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5041:59 - (ae-incompatible-release-tags) The symbol "__index" is marked as @public, but its signature references "Expression" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5447:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Field" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5457:5 - (ae-incompatible-release-tags) The symbol "map" is marked as @public, but its signature references "Expression" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5584:5 - (ae-incompatible-release-tags) The symbol "selections" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5601:5 - (ae-incompatible-release-tags) The symbol "orderings" is marked as @public, but its signature references "Ordering" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:6337:5 - (ae-incompatible-release-tags) The symbol "selectable" is marked as @public, but its signature references "Selectable" which is marked as @beta -// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:6386:5 - (ae-incompatible-release-tags) The symbol "condition" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:68:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:107:5 - (ae-incompatible-release-tags) The symbol "accumulators" is marked as @public, but its signature references "AliasedAggregate" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:112:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:769:5 - (ae-forgotten-export) The symbol "Query" needs to be exported by the entry point pipelines.d.ts +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:1086:5 - (ae-incompatible-release-tags) The symbol "groups" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:3251:5 - (ae-incompatible-release-tags) The symbol "field" is marked as @public, but its signature references "Field" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:4566:59 - (ae-incompatible-release-tags) The symbol "__index" is marked as @public, but its signature references "Expression" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5012:5 - (ae-incompatible-release-tags) The symbol "fields" is marked as @public, but its signature references "Field" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5022:5 - (ae-incompatible-release-tags) The symbol "map" is marked as @public, but its signature references "Expression" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5146:5 - (ae-incompatible-release-tags) The symbol "selections" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:5156:5 - (ae-incompatible-release-tags) The symbol "orderings" is marked as @public, but its signature references "Ordering" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:6053:5 - (ae-incompatible-release-tags) The symbol "selectable" is marked as @public, but its signature references "Selectable" which is marked as @beta +// /Users/markduckworth/projects/firebase-js-sdk/packages/firestore/dist/pipelines.d.ts:6095:5 - (ae-incompatible-release-tags) The symbol "condition" is marked as @public, but its signature references "BooleanExpression" which is marked as @beta // (No @packageDocumentation comment for this package) diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index 04d65f6c333..4f3bb1f3ca4 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -18,6 +18,8 @@ toc: path: /docs/reference/js/ai.arrayschema.md - title: AudioConversationController path: /docs/reference/js/ai.audioconversationcontroller.md + - title: AudioTranscriptionConfig + path: /docs/reference/js/ai.audiotranscriptionconfig.md - title: Backend path: /docs/reference/js/ai.backend.md - title: BaseParams @@ -202,6 +204,8 @@ toc: path: /docs/reference/js/ai.thinkingconfig.md - title: ToolConfig path: /docs/reference/js/ai.toolconfig.md + - title: Transcription + path: /docs/reference/js/ai.transcription.md - title: URLContext path: /docs/reference/js/ai.urlcontext.md - title: URLContextMetadata diff --git a/docs-devsite/ai.audiotranscriptionconfig.md b/docs-devsite/ai.audiotranscriptionconfig.md new file mode 100644 index 00000000000..ff53c9061ea --- /dev/null +++ b/docs-devsite/ai.audiotranscriptionconfig.md @@ -0,0 +1,19 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# AudioTranscriptionConfig interface +The audio transcription configuration. + +Signature: + +```typescript +export interface AudioTranscriptionConfig +``` diff --git a/docs-devsite/ai.enhancedgeneratecontentresponse.md b/docs-devsite/ai.enhancedgeneratecontentresponse.md index 9e947add0cb..609196d6039 100644 --- a/docs-devsite/ai.enhancedgeneratecontentresponse.md +++ b/docs-devsite/ai.enhancedgeneratecontentresponse.md @@ -24,6 +24,7 @@ export interface EnhancedGenerateContentResponse extends GenerateContentResponse | Property | Type | Description | | --- | --- | --- | | [functionCalls](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponsefunctioncalls) | () => [FunctionCall](./ai.functioncall.md#functioncall_interface)\[\] \| undefined | Aggregates and returns every [FunctionCall](./ai.functioncall.md#functioncall_interface) from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | +| [inferenceSource](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponseinferencesource) | [InferenceSource](./ai.md#inferencesource) | (Public Preview) Indicates whether inference happened on-device or in-cloud. | | [inlineDataParts](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponseinlinedataparts) | () => [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface)\[\] \| undefined | Aggregates and returns every [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface) from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | | [text](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponsetext) | () => string | Returns the text string from the response, if available. Throws if the prompt or candidate was blocked. | | [thoughtSummary](./ai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponsethoughtsummary) | () => string \| undefined | Aggregates and returns every [TextPart](./ai.textpart.md#textpart_interface) with their thought property set to true from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | @@ -38,6 +39,19 @@ Aggregates and returns every [FunctionCall](./ai.functioncall.md#functioncall_in functionCalls: () => FunctionCall[] | undefined; ``` +## EnhancedGenerateContentResponse.inferenceSource + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether inference happened on-device or in-cloud. + +Signature: + +```typescript +inferenceSource?: InferenceSource; +``` + ## EnhancedGenerateContentResponse.inlineDataParts Aggregates and returns every [InlineDataPart](./ai.inlinedatapart.md#inlinedatapart_interface) from the first candidate of [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). diff --git a/docs-devsite/ai.livegenerationconfig.md b/docs-devsite/ai.livegenerationconfig.md index 1a920afa1e7..2e842a34313 100644 --- a/docs-devsite/ai.livegenerationconfig.md +++ b/docs-devsite/ai.livegenerationconfig.md @@ -26,7 +26,9 @@ export interface LiveGenerationConfig | Property | Type | Description | | --- | --- | --- | | [frequencyPenalty](./ai.livegenerationconfig.md#livegenerationconfigfrequencypenalty) | number | (Public Preview) Frequency penalties. | +| [inputAudioTranscription](./ai.livegenerationconfig.md#livegenerationconfiginputaudiotranscription) | [AudioTranscriptionConfig](./ai.audiotranscriptionconfig.md#audiotranscriptionconfig_interface) | (Public Preview) Enables transcription of audio input.When enabled, the model will respond with transcriptions of your audio input in the inputTranscriptions property in [LiveServerContent](./ai.liveservercontent.md#liveservercontent_interface) messages. Note that the transcriptions are broken up across messages, so you may only receive small amounts of text per message. For example, if you ask the model "How are you today?", the model may transcribe that input across three messages, broken up as "How a", "re yo", "u today?". | | [maxOutputTokens](./ai.livegenerationconfig.md#livegenerationconfigmaxoutputtokens) | number | (Public Preview) Specifies the maximum number of tokens that can be generated in the response. The number of tokens per word varies depending on the language outputted. Is unbounded by default. | +| [outputAudioTranscription](./ai.livegenerationconfig.md#livegenerationconfigoutputaudiotranscription) | [AudioTranscriptionConfig](./ai.audiotranscriptionconfig.md#audiotranscriptionconfig_interface) | (Public Preview) Enables transcription of audio input.When enabled, the model will respond with transcriptions of its audio output in the outputTranscription property in [LiveServerContent](./ai.liveservercontent.md#liveservercontent_interface) messages. Note that the transcriptions are broken up across messages, so you may only receive small amounts of text per message. For example, if the model says "How are you today?", the model may transcribe that output across three messages, broken up as "How a", "re yo", "u today?". | | [presencePenalty](./ai.livegenerationconfig.md#livegenerationconfigpresencepenalty) | number | (Public Preview) Positive penalties. | | [responseModalities](./ai.livegenerationconfig.md#livegenerationconfigresponsemodalities) | [ResponseModality](./ai.md#responsemodality)\[\] | (Public Preview) The modalities of the response. | | [speechConfig](./ai.livegenerationconfig.md#livegenerationconfigspeechconfig) | [SpeechConfig](./ai.speechconfig.md#speechconfig_interface) | (Public Preview) Configuration for speech synthesis. | @@ -47,6 +49,21 @@ Frequency penalties. frequencyPenalty?: number; ``` +## LiveGenerationConfig.inputAudioTranscription + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Enables transcription of audio input. + +When enabled, the model will respond with transcriptions of your audio input in the `inputTranscriptions` property in [LiveServerContent](./ai.liveservercontent.md#liveservercontent_interface) messages. Note that the transcriptions are broken up across messages, so you may only receive small amounts of text per message. For example, if you ask the model "How are you today?", the model may transcribe that input across three messages, broken up as "How a", "re yo", "u today?". + +Signature: + +```typescript +inputAudioTranscription?: AudioTranscriptionConfig; +``` + ## LiveGenerationConfig.maxOutputTokens > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. @@ -60,6 +77,21 @@ Specifies the maximum number of tokens that can be generated in the response. Th maxOutputTokens?: number; ``` +## LiveGenerationConfig.outputAudioTranscription + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Enables transcription of audio input. + +When enabled, the model will respond with transcriptions of its audio output in the `outputTranscription` property in [LiveServerContent](./ai.liveservercontent.md#liveservercontent_interface) messages. Note that the transcriptions are broken up across messages, so you may only receive small amounts of text per message. For example, if the model says "How are you today?", the model may transcribe that output across three messages, broken up as "How a", "re yo", "u today?". + +Signature: + +```typescript +outputAudioTranscription?: AudioTranscriptionConfig; +``` + ## LiveGenerationConfig.presencePenalty > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. diff --git a/docs-devsite/ai.liveservercontent.md b/docs-devsite/ai.liveservercontent.md index f9c3ca1de79..6162601b8c2 100644 --- a/docs-devsite/ai.liveservercontent.md +++ b/docs-devsite/ai.liveservercontent.md @@ -25,11 +25,26 @@ export interface LiveServerContent | Property | Type | Description | | --- | --- | --- | +| [inputTranscription](./ai.liveservercontent.md#liveservercontentinputtranscription) | [Transcription](./ai.transcription.md#transcription_interface) | (Public Preview) Transcription of the audio that was input to the model. | | [interrupted](./ai.liveservercontent.md#liveservercontentinterrupted) | boolean | (Public Preview) Indicates whether the model was interrupted by the client. An interruption occurs when the client sends a message before the model finishes it's turn. This is undefined if the model was not interrupted. | | [modelTurn](./ai.liveservercontent.md#liveservercontentmodelturn) | [Content](./ai.content.md#content_interface) | (Public Preview) The content that the model has generated as part of the current conversation with the user. | +| [outputTranscription](./ai.liveservercontent.md#liveservercontentoutputtranscription) | [Transcription](./ai.transcription.md#transcription_interface) | (Public Preview) Transcription of the audio output from the model. | | [turnComplete](./ai.liveservercontent.md#liveservercontentturncomplete) | boolean | (Public Preview) Indicates whether the turn is complete. This is undefined if the turn is not complete. | | [type](./ai.liveservercontent.md#liveservercontenttype) | 'serverContent' | (Public Preview) | +## LiveServerContent.inputTranscription + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Transcription of the audio that was input to the model. + +Signature: + +```typescript +inputTranscription?: Transcription; +``` + ## LiveServerContent.interrupted > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. @@ -56,6 +71,19 @@ The content that the model has generated as part of the current conversation wit modelTurn?: Content; ``` +## LiveServerContent.outputTranscription + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Transcription of the audio output from the model. + +Signature: + +```typescript +outputTranscription?: Transcription; +``` + ## LiveServerContent.turnComplete > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. diff --git a/docs-devsite/ai.livesession.md b/docs-devsite/ai.livesession.md index 558c5eb3bd6..2f05fbc924b 100644 --- a/docs-devsite/ai.livesession.md +++ b/docs-devsite/ai.livesession.md @@ -39,9 +39,12 @@ export declare class LiveSession | [close()](./ai.livesession.md#livesessionclose) | | (Public Preview) Closes this session. All methods on this session will throw an error once this resolves. | | [receive()](./ai.livesession.md#livesessionreceive) | | (Public Preview) Yields messages received from the server. This can only be used by one consumer at a time. | | [send(request, turnComplete)](./ai.livesession.md#livesessionsend) | | (Public Preview) Sends content to the server. | +| [sendAudioRealtime(blob)](./ai.livesession.md#livesessionsendaudiorealtime) | | (Public Preview) Sends audio data to the server in realtime. | | [sendFunctionResponses(functionResponses)](./ai.livesession.md#livesessionsendfunctionresponses) | | (Public Preview) Sends function responses to the server. | | [sendMediaChunks(mediaChunks)](./ai.livesession.md#livesessionsendmediachunks) | | (Public Preview) Sends realtime input to the server. | -| [sendMediaStream(mediaChunkStream)](./ai.livesession.md#livesessionsendmediastream) | | (Public Preview) Sends a stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface). | +| [sendMediaStream(mediaChunkStream)](./ai.livesession.md#livesessionsendmediastream) | | (Public Preview) | +| [sendTextRealtime(text)](./ai.livesession.md#livesessionsendtextrealtime) | | (Public Preview) Sends text to the server in realtime. | +| [sendVideoRealtime(blob)](./ai.livesession.md#livesessionsendvideorealtime) | | (Public Preview) Sends video data to the server in realtime. | ## LiveSession.inConversation @@ -135,6 +138,45 @@ Promise<void> If this session has been closed. +## LiveSession.sendAudioRealtime() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends audio data to the server in realtime. + +The server requires that the audio data is base64-encoded 16-bit PCM at 16kHz little-endian. + +Signature: + +```typescript +sendAudioRealtime(blob: GenerativeContentBlob): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| blob | [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface) | The base64-encoded PCM data to send to the server in realtime. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + +### Example + + +```javascript +// const pcmData = ... base64-encoded 16-bit PCM at 16kHz little-endian. +const blob = { mimeType: "audio/pcm", data: pcmData }; +liveSession.sendAudioRealtime(blob); + +``` + ## LiveSession.sendFunctionResponses() > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. @@ -167,6 +209,11 @@ If this session has been closed. > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > +> Warning: This API is now obsolete. +> +> Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead. +> + Sends realtime input to the server. Signature: @@ -194,7 +241,12 @@ If this session has been closed. > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -Sends a stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface). +> Warning: This API is now obsolete. +> +> Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead. +> +> Sends a stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface). +> Signature: @@ -216,3 +268,77 @@ Promise<void> If this session has been closed. +## LiveSession.sendTextRealtime() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends text to the server in realtime. + +Signature: + +```typescript +sendTextRealtime(text: string): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| text | string | The text data to send. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + +### Example + + +```javascript +liveSession.sendTextRealtime("Hello, how are you?"); + +``` + +## LiveSession.sendVideoRealtime() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends video data to the server in realtime. + +The server requires that the video is sent as individual video frames at 1 FPS. It is recommended to set `mimeType` to `image/jpeg`. + +Signature: + +```typescript +sendVideoRealtime(blob: GenerativeContentBlob): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| blob | [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface) | The base64-encoded video data to send to the server in realtime. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + +### Example + + +```javascript +// const videoFrame = ... base64-encoded JPEG data +const blob = { mimeType: "image/jpeg", data: videoFrame }; +liveSession.sendVideoRealtime(blob); + +``` + diff --git a/docs-devsite/ai.md b/docs-devsite/ai.md index db6148ee88c..79902cab4e7 100644 --- a/docs-devsite/ai.md +++ b/docs-devsite/ai.md @@ -56,6 +56,7 @@ The Firebase AI Web SDK. | [AI](./ai.ai.md#ai_interface) | An instance of the Firebase AI SDK.Do not create this instance directly. Instead, use [getAI()](./ai.md#getai_a94a413). | | [AIOptions](./ai.aioptions.md#aioptions_interface) | Options for initializing the AI service using [getAI()](./ai.md#getai_a94a413). This allows specifying which backend to use (Vertex AI Gemini API or Gemini Developer API) and configuring its specific options (like location for Vertex AI). | | [AudioConversationController](./ai.audioconversationcontroller.md#audioconversationcontroller_interface) | (Public Preview) A controller for managing an active audio conversation. | +| [AudioTranscriptionConfig](./ai.audiotranscriptionconfig.md#audiotranscriptionconfig_interface) | The audio transcription configuration. | | [BaseParams](./ai.baseparams.md#baseparams_interface) | Base parameters for a number of methods. | | [ChromeAdapter](./ai.chromeadapter.md#chromeadapter_interface) | (Public Preview) Defines an inference "backend" that uses Chrome's on-device model, and encapsulates logic for detecting when on-device inference is possible.These methods should not be called directly by the user. | | [Citation](./ai.citation.md#citation_interface) | A single citation. | @@ -134,6 +135,7 @@ The Firebase AI Web SDK. | [TextPart](./ai.textpart.md#textpart_interface) | Content part interface if the part represents a text string. | | [ThinkingConfig](./ai.thinkingconfig.md#thinkingconfig_interface) | Configuration for "thinking" behavior of compatible Gemini models.Certain models utilize a thinking process before generating a response. This allows them to reason through complex problems and plan a more coherent and accurate answer. | | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | Tool config. This config is shared for all tools provided in the request. | +| [Transcription](./ai.transcription.md#transcription_interface) | (Public Preview) Transcription of audio. This can be returned from a [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) if transcription is enabled with the inputAudioTranscription or outputAudioTranscription properties on the [LiveGenerationConfig](./ai.livegenerationconfig.md#livegenerationconfig_interface). | | [URLContext](./ai.urlcontext.md#urlcontext_interface) | (Public Preview) Specifies the URL Context configuration. | | [URLContextMetadata](./ai.urlcontextmetadata.md#urlcontextmetadata_interface) | (Public Preview) Metadata related to [URLContextTool](./ai.urlcontexttool.md#urlcontexttool_interface). | | [URLContextTool](./ai.urlcontexttool.md#urlcontexttool_interface) | (Public Preview) A tool that allows you to provide additional context to the models in the form of public web URLs. By including URLs in your request, the Gemini model will access the content from those pages to inform and enhance its response. | @@ -162,6 +164,7 @@ The Firebase AI Web SDK. | [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel) | A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | | [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel) | A filter level controlling how aggressively to filter sensitive content.Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, violence, sexual, derogatory, and toxic). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. | | [InferenceMode](./ai.md#inferencemode) | (Public Preview) Determines whether inference happens on-device or in-cloud. | +| [InferenceSource](./ai.md#inferencesource) | (Public Preview) Indicates whether inference happened on-device or in-cloud. | | [Language](./ai.md#language) | (Public Preview) The programming language of the code. | | [LiveResponseType](./ai.md#liveresponsetype) | (Public Preview) The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). | | [Modality](./ai.md#modality) | Content part modality. | @@ -189,6 +192,7 @@ The Firebase AI Web SDK. | [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel) | A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | | [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel) | A filter level controlling how aggressively to filter sensitive content.Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, violence, sexual, derogatory, and toxic). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. | | [InferenceMode](./ai.md#inferencemode) | (Public Preview) Determines whether inference happens on-device or in-cloud. | +| [InferenceSource](./ai.md#inferencesource) | (Public Preview) Indicates whether inference happened on-device or in-cloud. | | [Language](./ai.md#language) | (Public Preview) The programming language of the code. | | [LanguageModelMessageContentValue](./ai.md#languagemodelmessagecontentvalue) | (Public Preview) Content formats that can be provided as on-device message content. | | [LanguageModelMessageRole](./ai.md#languagemodelmessagerole) | (Public Preview) Allowable roles for on-device language model usage. | @@ -643,6 +647,22 @@ InferenceMode: { } ``` +## InferenceSource + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether inference happened on-device or in-cloud. + +Signature: + +```typescript +InferenceSource: { + readonly ON_DEVICE: "on_device"; + readonly IN_CLOUD: "in_cloud"; +} +``` + ## Language > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. @@ -926,6 +946,19 @@ Determines whether inference happens on-device or in-cloud. export type InferenceMode = (typeof InferenceMode)[keyof typeof InferenceMode]; ``` +## InferenceSource + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether inference happened on-device or in-cloud. + +Signature: + +```typescript +export type InferenceSource = (typeof InferenceSource)[keyof typeof InferenceSource]; +``` + ## Language > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. diff --git a/docs-devsite/ai.transcription.md b/docs-devsite/ai.transcription.md new file mode 100644 index 00000000000..7ab6a360d5e --- /dev/null +++ b/docs-devsite/ai.transcription.md @@ -0,0 +1,41 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# Transcription interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Transcription of audio. This can be returned from a [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) if transcription is enabled with the `inputAudioTranscription` or `outputAudioTranscription` properties on the [LiveGenerationConfig](./ai.livegenerationconfig.md#livegenerationconfig_interface). + +Signature: + +```typescript +export interface Transcription +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [text](./ai.transcription.md#transcriptiontext) | string | (Public Preview) The text transcription of the audio. | + +## Transcription.text + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The text transcription of the audio. + +Signature: + +```typescript +text?: string; +``` diff --git a/docs-devsite/auth.md b/docs-devsite/auth.md index 1b3938ef4eb..97c12052271 100644 --- a/docs-devsite/auth.md +++ b/docs-devsite/auth.md @@ -1914,6 +1914,7 @@ AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY: { readonly MISSING_MFA_INFO: "auth/missing-multi-factor-info"; readonly MISSING_MFA_SESSION: "auth/missing-multi-factor-session"; readonly MISSING_PHONE_NUMBER: "auth/missing-phone-number"; + readonly MISSING_PASSWORD: "auth/missing-password"; readonly MISSING_SESSION_INFO: "auth/missing-verification-id"; readonly MODULE_DESTROYED: "auth/app-deleted"; readonly NEED_CONFIRMATION: "auth/account-exists-with-different-credential"; diff --git a/integration/compat-interop/package.json b/integration/compat-interop/package.json index a88d4734e57..9660e71e652 100644 --- a/integration/compat-interop/package.json +++ b/integration/compat-interop/package.json @@ -8,12 +8,12 @@ "test:debug": "karma start --browsers Chrome --auto-watch" }, "dependencies": { - "@firebase/app": "0.14.4", - "@firebase/app-compat": "0.5.4", + "@firebase/app": "0.14.5", + "@firebase/app-compat": "0.5.5", "@firebase/analytics": "0.10.19", "@firebase/analytics-compat": "0.2.25", - "@firebase/auth": "1.11.0", - "@firebase/auth-compat": "0.6.0", + "@firebase/auth": "1.11.1", + "@firebase/auth-compat": "0.6.1", "@firebase/functions": "0.13.1", "@firebase/functions-compat": "0.4.1", "@firebase/messaging": "0.12.23", diff --git a/integration/firestore/package.json b/integration/firestore/package.json index c050d38ec94..26e7f66f193 100644 --- a/integration/firestore/package.json +++ b/integration/firestore/package.json @@ -14,7 +14,7 @@ "test:memory:debug": "yarn build:memory; karma start --auto-watch --browsers Chrome" }, "dependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "@firebase/firestore": "4.9.2" }, "devDependencies": { diff --git a/integration/messaging/package.json b/integration/messaging/package.json index 5c8358e6627..1c633bede91 100644 --- a/integration/messaging/package.json +++ b/integration/messaging/package.json @@ -9,7 +9,7 @@ "test:manual": "mocha --exit" }, "devDependencies": { - "firebase": "12.4.0", + "firebase": "12.5.0", "chai": "4.5.0", "chromedriver": "119.0.1", "express": "4.21.2", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index af19b6f354e..1c3959ccdf1 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,20 @@ # @firebase/ai +## 2.5.0 + +### Minor Changes + +- [`22e0a1a`](https://github.com/firebase/firebase-js-sdk/commit/22e0a1adbc994196690bd020472d119c1a3d200b) [#9291](https://github.com/firebase/firebase-js-sdk/pull/9291) - Deprecate `sendMediaChunks()` and `sendMediaStream()`. Instead, use the new methods added to the `LiveSession` class. + Add `sendTextRealtime()`, `sendAudioReatime()`, and `sendVideoRealtime()` to the `LiveSession` class. + +- [`bc5a7c4`](https://github.com/firebase/firebase-js-sdk/commit/bc5a7c4a74e72e9218d1435bfe50711c77b47cbd) [#9330](https://github.com/firebase/firebase-js-sdk/pull/9330) - Add support for audio transcriptions in the Live API. + +- [`c8263c4`](https://github.com/firebase/firebase-js-sdk/commit/c8263c471db4df1b0e23f0d2a11c69fd6b920e2e) [#9315](https://github.com/firebase/firebase-js-sdk/pull/9315) - Add `inferenceSource` to the response from `generateContent` and `generateContentStream`. This property indicates whether on-device or in-cloud inference was used to generate the result. + +### Patch Changes + +- [`44d9891`](https://github.com/firebase/firebase-js-sdk/commit/44d9891f93298ab4bcef5170c40c235831af0276) [#9314](https://github.com/firebase/firebase-js-sdk/pull/9314) - Fix logic for merging default `onDeviceParams` with user-provided `onDeviceParams`. + ## 2.4.0 ### Minor Changes diff --git a/packages/ai/integration/live.test.ts b/packages/ai/integration/live.test.ts index caa18970ab7..6b50fe65222 100644 --- a/packages/ai/integration/live.test.ts +++ b/packages/ai/integration/live.test.ts @@ -154,6 +154,45 @@ describe('Live', function () { }); }); + describe('sendTextRealtime()', () => { + it('should send a single text chunk and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + await session.sendTextRealtime('Are you an AI? Yes or No.'); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + describe('sendAudioRealtime()', () => { + it('should send a single audio chunk and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + await session.sendAudioRealtime({ + data: HELLO_AUDIO_PCM_BASE64, // "Hey, can you hear me?" + mimeType: 'audio/pcm' + }); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + describe('sendMediaChunks()', () => { it('should send a single audio chunk and receive a response', async () => { const model = getLiveGenerativeModel(testConfig.ai, { @@ -231,6 +270,56 @@ describe('Live', function () { }); }); + describe('Transcripts', async () => { + it('should receive transcript of audio input', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: { + responseModalities: [ResponseModality.AUDIO], + inputAudioTranscription: {}, + outputAudioTranscription: {} + } + }); + const session = await model.connect(); + const stream = session.receive(); + + await session.sendAudioRealtime({ + data: HELLO_AUDIO_PCM_BASE64, + mimeType: 'audio/pcm' + }); + + let aggregatedInputTranscription = ''; + let aggregatedOutputTranscription = ''; + let result = await stream.next(); + while (!result.done) { + const chunk = result.value as + | LiveServerContent + | LiveServerToolCall + | LiveServerToolCallCancellation; + if (chunk.type === 'serverContent') { + if (chunk.turnComplete) { + break; + } + + if (chunk.inputTranscription) { + aggregatedInputTranscription += chunk.inputTranscription?.text; + } + if (chunk.outputTranscription) { + aggregatedOutputTranscription += + chunk.outputTranscription?.text; + } + } + + result = await stream.next(); + } + + expect(aggregatedInputTranscription).to.not.be.empty; + expect(aggregatedOutputTranscription).to.not.be.empty; + + await session.close(); + }); + }); + /** * These tests are currently very unreliable. Their behavior seems to change frequently. * Skipping them for now. diff --git a/packages/ai/package.json b/packages/ai/package.json index c5a27484c3d..dcb6f11fdbf 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/ai", - "version": "2.4.0", + "version": "2.5.0", "description": "The Firebase AI SDK", "author": "Firebase (https://firebase.google.com/)", "engines": { @@ -60,7 +60,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "@rollup/plugin-json": "6.1.0", "rollup": "2.79.2", "rollup-plugin-replace": "2.2.0", diff --git a/packages/ai/src/api-browser.test.ts b/packages/ai/src/api-browser.test.ts new file mode 100644 index 00000000000..74cfea05f4f --- /dev/null +++ b/packages/ai/src/api-browser.test.ts @@ -0,0 +1,48 @@ +/** + * @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 { getAI, getGenerativeModel } from './api'; +import { expect } from 'chai'; +import { InferenceMode } from './public-types'; +import { getFullApp } from '../test-utils/get-fake-firebase-services'; +import { DEFAULT_HYBRID_IN_CLOUD_MODEL } from './constants'; +import { factory } from './factory-browser'; + +/** + * Browser-only top level API tests using a factory that provides + * a ChromeAdapter. + */ +describe('Top level API', () => { + describe('getAI()', () => { + it('getGenerativeModel with HybridParams sets a default model', () => { + const ai = getAI(getFullApp({ apiKey: 'key', appId: 'id' }, factory)); + const genModel = getGenerativeModel(ai, { + mode: InferenceMode.ONLY_ON_DEVICE + }); + expect(genModel.model).to.equal( + `models/${DEFAULT_HYBRID_IN_CLOUD_MODEL}` + ); + }); + it('getGenerativeModel with HybridParams honors a model override', () => { + const ai = getAI(getFullApp({ apiKey: 'key', appId: 'id' }, factory)); + const genModel = getGenerativeModel(ai, { + mode: InferenceMode.PREFER_ON_DEVICE, + inCloudParams: { model: 'my-model' } + }); + expect(genModel.model).to.equal('models/my-model'); + }); + }); +}); diff --git a/packages/ai/src/api.test.ts b/packages/ai/src/api.test.ts index 65ecbbdcba8..3854f010fc7 100644 --- a/packages/ai/src/api.test.ts +++ b/packages/ai/src/api.test.ts @@ -29,7 +29,7 @@ import { AI } from './public-types'; import { GenerativeModel } from './models/generative-model'; import { GoogleAIBackend, VertexAIBackend } from './backend'; import { getFullApp } from '../test-utils/get-fake-firebase-services'; -import { AI_TYPE, DEFAULT_HYBRID_IN_CLOUD_MODEL } from './constants'; +import { AI_TYPE } from './constants'; const fakeAI: AI = { app: { @@ -144,21 +144,6 @@ describe('Top level API', () => { expect(genModel).to.be.an.instanceOf(GenerativeModel); expect(genModel.model).to.equal('publishers/google/models/my-model'); }); - it('getGenerativeModel with HybridParams sets a default model', () => { - const genModel = getGenerativeModel(fakeAI, { - mode: 'only_on_device' - }); - expect(genModel.model).to.equal( - `publishers/google/models/${DEFAULT_HYBRID_IN_CLOUD_MODEL}` - ); - }); - it('getGenerativeModel with HybridParams honors a model override', () => { - const genModel = getGenerativeModel(fakeAI, { - mode: 'prefer_on_device', - inCloudParams: { model: 'my-model' } - }); - expect(genModel.model).to.equal('publishers/google/models/my-model'); - }); it('getImagenModel throws if no model is provided', () => { try { getImagenModel(fakeAI, {} as ImagenModelParams); diff --git a/packages/ai/src/factory-node.ts b/packages/ai/src/factory-node.ts new file mode 100644 index 00000000000..212ca18a824 --- /dev/null +++ b/packages/ai/src/factory-node.ts @@ -0,0 +1,46 @@ +/** + * @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 { + ComponentContainer, + InstanceFactoryOptions +} from '@firebase/component'; +import { AIError } from './errors'; +import { decodeInstanceIdentifier } from './helpers'; +import { AIService } from './service'; +import { AIErrorCode } from './types'; + +export function factory( + container: ComponentContainer, + { instanceIdentifier }: InstanceFactoryOptions +): AIService { + if (!instanceIdentifier) { + throw new AIError( + AIErrorCode.ERROR, + 'AIService instance identifier is undefined.' + ); + } + + const backend = decodeInstanceIdentifier(instanceIdentifier); + + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + const auth = container.getProvider('auth-internal'); + const appCheckProvider = container.getProvider('app-check-internal'); + + return new AIService(app, backend, auth, appCheckProvider); +} diff --git a/packages/ai/src/index.node.ts b/packages/ai/src/index.node.ts index bb05fdcea45..07d95e7d30e 100644 --- a/packages/ai/src/index.node.ts +++ b/packages/ai/src/index.node.ts @@ -22,36 +22,16 @@ */ import { registerVersion, _registerComponent } from '@firebase/app'; -import { AIService } from './service'; import { AI_TYPE } from './constants'; import { Component, ComponentType } from '@firebase/component'; import { name, version } from '../package.json'; -import { decodeInstanceIdentifier } from './helpers'; -import { AIError } from './errors'; -import { AIErrorCode } from './public-types'; +import { factory } from './factory-node'; function registerAI(): void { _registerComponent( - new Component( - AI_TYPE, - (container, { instanceIdentifier }) => { - if (!instanceIdentifier) { - throw new AIError( - AIErrorCode.ERROR, - 'AIService instance identifier is undefined.' - ); - } - - const backend = decodeInstanceIdentifier(instanceIdentifier); - - // getImmediate for FirebaseApp will always succeed - const app = container.getProvider('app').getImmediate(); - const auth = container.getProvider('auth-internal'); - const appCheckProvider = container.getProvider('app-check-internal'); - return new AIService(app, backend, auth, appCheckProvider); - }, - ComponentType.PUBLIC - ).setMultipleInstances(true) + new Component(AI_TYPE, factory, ComponentType.PUBLIC).setMultipleInstances( + true + ) ); registerVersion(name, version, 'node'); diff --git a/packages/ai/src/methods/chat-session.test.ts b/packages/ai/src/methods/chat-session.test.ts index e92aa057af1..1273d02876c 100644 --- a/packages/ai/src/methods/chat-session.test.ts +++ b/packages/ai/src/methods/chat-session.test.ts @@ -20,11 +20,11 @@ import { match, restore, stub, useFakeTimers } from 'sinon'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; import * as generateContentMethods from './generate-content'; -import { Content, GenerateContentStreamResult, InferenceMode } from '../types'; +import { Content, GenerateContentStreamResult } from '../types'; import { ChatSession } from './chat-session'; import { ApiSettings } from '../types/internal'; import { VertexAIBackend } from '../backend'; -import { ChromeAdapterImpl } from './chrome-adapter'; +import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; use(sinonChai); use(chaiAsPromised); @@ -37,12 +37,6 @@ const fakeApiSettings: ApiSettings = { backend: new VertexAIBackend() }; -const fakeChromeAdapter = new ChromeAdapterImpl( - // @ts-expect-error - undefined, - InferenceMode.PREFER_ON_DEVICE -); - describe('ChatSession', () => { afterEach(() => { restore(); diff --git a/packages/ai/src/methods/chrome-adapter-browser.test.ts b/packages/ai/src/methods/chrome-adapter-browser.test.ts index 5d5b2344ab6..e37a08bf1a9 100644 --- a/packages/ai/src/methods/chrome-adapter-browser.test.ts +++ b/packages/ai/src/methods/chrome-adapter-browser.test.ts @@ -78,6 +78,63 @@ describe('ChromeAdapter', () => { expectedInputs: [{ type: 'image' }] }); }); + it('sets image as expected input type by default even if other onDeviceParams params are set', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const availabilityStub = stub( + languageModelProvider, + 'availability' + ).resolves(Availability.AVAILABLE); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { + promptOptions: {} + } + ); + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [{ text: 'hi' }] + } + ] + }); + expect(availabilityStub).to.have.been.calledWith({ + expectedInputs: [{ type: 'image' }] + }); + }); + it('sets image as expected input type by default even if other createOptions params are set', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const availabilityStub = stub( + languageModelProvider, + 'availability' + ).resolves(Availability.AVAILABLE); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { + createOptions: { + topK: 22 + } + } + ); + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [{ text: 'hi' }] + } + ] + }); + expect(availabilityStub).to.have.been.calledWith({ + topK: 22, + expectedInputs: [{ type: 'image' }] + }); + }); it('honors explicitly set expected inputs', async () => { const languageModelProvider = { availability: () => Promise.resolve(Availability.AVAILABLE) diff --git a/packages/ai/src/methods/chrome-adapter.ts b/packages/ai/src/methods/chrome-adapter.ts index a0ab509e335..839276814bb 100644 --- a/packages/ai/src/methods/chrome-adapter.ts +++ b/packages/ai/src/methods/chrome-adapter.ts @@ -31,11 +31,15 @@ import { ChromeAdapter } from '../types/chrome-adapter'; import { Availability, LanguageModel, + LanguageModelExpected, LanguageModelMessage, LanguageModelMessageContent, LanguageModelMessageRole } from '../types/language-model'; +// Defaults to support image inputs for convenience. +const defaultExpectedInputs: LanguageModelExpected[] = [{ type: 'image' }]; + /** * Defines an inference "backend" that uses Chrome's on-device model, * and encapsulates logic for detecting when on-device inference is @@ -47,16 +51,28 @@ export class ChromeAdapterImpl implements ChromeAdapter { private isDownloading = false; private downloadPromise: Promise | undefined; private oldSession: LanguageModel | undefined; + onDeviceParams: OnDeviceParams = { + createOptions: { + expectedInputs: defaultExpectedInputs + } + }; constructor( public languageModelProvider: LanguageModel, public mode: InferenceMode, - public onDeviceParams: OnDeviceParams = { - createOptions: { - // Defaults to support image inputs for convenience. - expectedInputs: [{ type: 'image' }] + onDeviceParams?: OnDeviceParams + ) { + if (onDeviceParams) { + this.onDeviceParams = onDeviceParams; + if (!this.onDeviceParams.createOptions) { + this.onDeviceParams.createOptions = { + expectedInputs: defaultExpectedInputs + }; + } else if (!this.onDeviceParams.createOptions.expectedInputs) { + this.onDeviceParams.createOptions.expectedInputs = + defaultExpectedInputs; } } - ) {} + } /** * Checks if a given request can be made on-device. diff --git a/packages/ai/src/methods/count-tokens.test.ts b/packages/ai/src/methods/count-tokens.test.ts index aabf06a841a..84976d00ac9 100644 --- a/packages/ai/src/methods/count-tokens.test.ts +++ b/packages/ai/src/methods/count-tokens.test.ts @@ -27,7 +27,7 @@ import { ApiSettings } from '../types/internal'; import { Task } from '../requests/request'; import { mapCountTokensRequest } from '../googleai-mappers'; import { GoogleAIBackend, VertexAIBackend } from '../backend'; -import { ChromeAdapterImpl } from './chrome-adapter'; +import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; use(sinonChai); use(chaiAsPromised); @@ -52,12 +52,6 @@ const fakeRequestParams: CountTokensRequest = { contents: [{ parts: [{ text: 'hello' }], role: 'user' }] }; -const fakeChromeAdapter = new ChromeAdapterImpl( - // @ts-expect-error - undefined, - InferenceMode.PREFER_ON_DEVICE -); - describe('countTokens()', () => { afterEach(() => { restore(); @@ -197,11 +191,10 @@ describe('countTokens()', () => { }); }); it('throws if mode is ONLY_ON_DEVICE', async () => { - const chromeAdapter = new ChromeAdapterImpl( - // @ts-expect-error - undefined, - InferenceMode.ONLY_ON_DEVICE - ); + const chromeAdapter = { + ...fakeChromeAdapter, + mode: InferenceMode.ONLY_ON_DEVICE + }; await expect( countTokens(fakeApiSettings, 'model', fakeRequestParams, chromeAdapter) ).to.be.rejectedWith( diff --git a/packages/ai/src/methods/count-tokens.ts b/packages/ai/src/methods/count-tokens.ts index ecd86a82912..c6041a0bb99 100644 --- a/packages/ai/src/methods/count-tokens.ts +++ b/packages/ai/src/methods/count-tokens.ts @@ -28,7 +28,6 @@ import { ApiSettings } from '../types/internal'; import * as GoogleAIMapper from '../googleai-mappers'; import { BackendType } from '../public-types'; import { ChromeAdapter } from '../types/chrome-adapter'; -import { ChromeAdapterImpl } from './chrome-adapter'; export async function countTokensOnCloud( apiSettings: ApiSettings, @@ -61,9 +60,7 @@ export async function countTokens( chromeAdapter?: ChromeAdapter, requestOptions?: RequestOptions ): Promise { - if ( - (chromeAdapter as ChromeAdapterImpl)?.mode === InferenceMode.ONLY_ON_DEVICE - ) { + if (chromeAdapter?.mode === InferenceMode.ONLY_ON_DEVICE) { throw new AIError( AIErrorCode.UNSUPPORTED, 'countTokens() is not supported for on-device models.' diff --git a/packages/ai/src/methods/generate-content.test.ts b/packages/ai/src/methods/generate-content.test.ts index 40dc7c7b36e..33a9ae5f5e3 100644 --- a/packages/ai/src/methods/generate-content.test.ts +++ b/packages/ai/src/methods/generate-content.test.ts @@ -28,7 +28,6 @@ import { HarmBlockMethod, HarmBlockThreshold, HarmCategory, - InferenceMode, Language, Outcome } from '../types'; @@ -37,17 +36,11 @@ import { Task } from '../requests/request'; import { AIError } from '../api'; import { mapGenerateContentRequest } from '../googleai-mappers'; import { GoogleAIBackend, VertexAIBackend } from '../backend'; -import { ChromeAdapterImpl } from './chrome-adapter'; +import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; use(sinonChai); use(chaiAsPromised); -const fakeChromeAdapter = new ChromeAdapterImpl( - // @ts-expect-error - undefined, - InferenceMode.PREFER_ON_DEVICE -); - const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', diff --git a/packages/ai/src/methods/generate-content.ts b/packages/ai/src/methods/generate-content.ts index 0e65b479343..a2fb29e20d1 100644 --- a/packages/ai/src/methods/generate-content.ts +++ b/packages/ai/src/methods/generate-content.ts @@ -57,14 +57,14 @@ export async function generateContentStream( chromeAdapter?: ChromeAdapter, requestOptions?: RequestOptions ): Promise { - const response = await callCloudOrDevice( + const callResult = await callCloudOrDevice( params, chromeAdapter, () => chromeAdapter!.generateContentStream(params), () => generateContentStreamOnCloud(apiSettings, model, params, requestOptions) ); - return processStream(response, apiSettings); // TODO: Map streaming responses + return processStream(callResult.response, apiSettings); // TODO: Map streaming responses } async function generateContentOnCloud( @@ -93,18 +93,19 @@ export async function generateContent( chromeAdapter?: ChromeAdapter, requestOptions?: RequestOptions ): Promise { - const response = await callCloudOrDevice( + const callResult = await callCloudOrDevice( params, chromeAdapter, () => chromeAdapter!.generateContent(params), () => generateContentOnCloud(apiSettings, model, params, requestOptions) ); const generateContentResponse = await processGenerateContentResponse( - response, + callResult.response, apiSettings ); const enhancedResponse = createEnhancedContentResponse( - generateContentResponse + generateContentResponse, + callResult.inferenceSource ); return { response: enhancedResponse diff --git a/packages/ai/src/methods/live-session-helpers.test.ts b/packages/ai/src/methods/live-session-helpers.test.ts index cad0475b358..a62315c701d 100644 --- a/packages/ai/src/methods/live-session-helpers.test.ts +++ b/packages/ai/src/methods/live-session-helpers.test.ts @@ -65,7 +65,7 @@ class MockLiveSession { isClosed = false; inConversation = false; send = sinon.stub(); - sendMediaChunks = sinon.stub(); + sendAudioRealtime = sinon.stub(); sendFunctionResponses = sinon.stub(); messageGenerator = new MockMessageGenerator(); receive = (): MockMessageGenerator => this.messageGenerator; @@ -226,8 +226,8 @@ describe('Audio Conversation Helpers', () => { await clock.tickAsync(1); - expect(liveSession.sendMediaChunks).to.have.been.calledOnce; - const [sentChunk] = liveSession.sendMediaChunks.getCall(0).args[0]; + expect(liveSession.sendAudioRealtime).to.have.been.calledOnce; + const sentChunk = liveSession.sendAudioRealtime.getCall(0).args[0]; expect(sentChunk.mimeType).to.equal('audio/pcm'); expect(sentChunk.data).to.be.a('string'); await controller.stop(); diff --git a/packages/ai/src/methods/live-session-helpers.ts b/packages/ai/src/methods/live-session-helpers.ts index b3907d6219b..cb3be493f5d 100644 --- a/packages/ai/src/methods/live-session-helpers.ts +++ b/packages/ai/src/methods/live-session-helpers.ts @@ -184,7 +184,7 @@ export class AudioConversationRunner { mimeType: 'audio/pcm', data: base64 }; - void this.liveSession.sendMediaChunks([chunk]); + void this.liveSession.sendAudioRealtime(chunk); }; } diff --git a/packages/ai/src/methods/live-session.test.ts b/packages/ai/src/methods/live-session.test.ts index 7454b1208c9..428e92ec770 100644 --- a/packages/ai/src/methods/live-session.test.ts +++ b/packages/ai/src/methods/live-session.test.ts @@ -110,6 +110,42 @@ describe('LiveSession', () => { }); }); + describe('sendTextRealtime()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const text = 'foo'; + await session.sendTextRealtime(text); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { text } + }); + }); + }); + + describe('sendAudioRealtime()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const blob = { data: 'abcdef', mimeType: 'audio/pcm' }; + await session.sendAudioRealtime(blob); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { audio: blob } + }); + }); + }); + + describe('sendVideoRealtime()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const blob = { data: 'abcdef', mimeType: 'image/jpeg' }; + await session.sendVideoRealtime(blob); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { video: blob } + }); + }); + }); + describe('sendMediaChunks()', () => { it('should send a correctly formatted realtimeInput message', async () => { const chunks = [{ data: 'base64', mimeType: 'audio/webm' }]; diff --git a/packages/ai/src/methods/live-session.ts b/packages/ai/src/methods/live-session.ts index 92d325e2f0d..1db5e3d4dd4 100644 --- a/packages/ai/src/methods/live-session.ts +++ b/packages/ai/src/methods/live-session.ts @@ -96,14 +96,19 @@ export class LiveSession { } /** - * Sends realtime input to the server. + * Sends text to the server in realtime. * - * @param mediaChunks - The media chunks to send. + * @example + * ```javascript + * liveSession.sendTextRealtime("Hello, how are you?"); + * ``` + * + * @param text - The text data to send. * @throws If this session has been closed. * * @beta */ - async sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise { + async sendTextRealtime(text: string): Promise { if (this.isClosed) { throw new AIError( AIErrorCode.REQUEST_ERROR, @@ -111,27 +116,33 @@ export class LiveSession { ); } - // The backend does not support sending more than one mediaChunk in one message. - // Work around this limitation by sending mediaChunks in separate messages. - mediaChunks.forEach(mediaChunk => { - const message: _LiveClientRealtimeInput = { - realtimeInput: { mediaChunks: [mediaChunk] } - }; - this.webSocketHandler.send(JSON.stringify(message)); - }); + const message: _LiveClientRealtimeInput = { + realtimeInput: { + text + } + }; + this.webSocketHandler.send(JSON.stringify(message)); } /** - * Sends function responses to the server. + * Sends audio data to the server in realtime. * - * @param functionResponses - The function responses to send. + * @remarks The server requires that the audio data is base64-encoded 16-bit PCM at 16kHz + * little-endian. + * + * @example + * ```javascript + * // const pcmData = ... base64-encoded 16-bit PCM at 16kHz little-endian. + * const blob = { mimeType: "audio/pcm", data: pcmData }; + * liveSession.sendAudioRealtime(blob); + * ``` + * + * @param blob - The base64-encoded PCM data to send to the server in realtime. * @throws If this session has been closed. * * @beta */ - async sendFunctionResponses( - functionResponses: FunctionResponse[] - ): Promise { + async sendAudioRealtime(blob: GenerativeContentBlob): Promise { if (this.isClosed) { throw new AIError( AIErrorCode.REQUEST_ERROR, @@ -139,25 +150,32 @@ export class LiveSession { ); } - const message: _LiveClientToolResponse = { - toolResponse: { - functionResponses + const message: _LiveClientRealtimeInput = { + realtimeInput: { + audio: blob } }; this.webSocketHandler.send(JSON.stringify(message)); } /** - * Sends a stream of {@link GenerativeContentBlob}. + * Sends video data to the server in realtime. * - * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send. + * @remarks The server requires that the video is sent as individual video frames at 1 FPS. It + * is recommended to set `mimeType` to `image/jpeg`. + * + * @example + * ```javascript + * // const videoFrame = ... base64-encoded JPEG data + * const blob = { mimeType: "image/jpeg", data: videoFrame }; + * liveSession.sendVideoRealtime(blob); + * ``` + * @param blob - The base64-encoded video data to send to the server in realtime. * @throws If this session has been closed. * * @beta */ - async sendMediaStream( - mediaChunkStream: ReadableStream - ): Promise { + async sendVideoRealtime(blob: GenerativeContentBlob): Promise { if (this.isClosed) { throw new AIError( AIErrorCode.REQUEST_ERROR, @@ -165,25 +183,38 @@ export class LiveSession { ); } - const reader = mediaChunkStream.getReader(); - while (true) { - try { - const { done, value } = await reader.read(); + const message: _LiveClientRealtimeInput = { + realtimeInput: { + video: blob + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } - if (done) { - break; - } else if (!value) { - throw new Error('Missing chunk in reader, but reader is not done.'); - } + /** + * Sends function responses to the server. + * + * @param functionResponses - The function responses to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendFunctionResponses( + functionResponses: FunctionResponse[] + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } - await this.sendMediaChunks([value]); - } catch (e) { - // Re-throw any errors that occur during stream consumption or sending. - const message = - e instanceof Error ? e.message : 'Error processing media stream.'; - throw new AIError(AIErrorCode.REQUEST_ERROR, message); + const message: _LiveClientToolResponse = { + toolResponse: { + functionResponses } - } + }; + this.webSocketHandler.send(JSON.stringify(message)); } /** @@ -259,4 +290,73 @@ export class LiveSession { await this.webSocketHandler.close(1000, 'Client closed session.'); } } + + /** + * Sends realtime input to the server. + * + * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead. + * + * @param mediaChunks - The media chunks to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + // The backend does not support sending more than one mediaChunk in one message. + // Work around this limitation by sending mediaChunks in separate messages. + mediaChunks.forEach(mediaChunk => { + const message: _LiveClientRealtimeInput = { + realtimeInput: { mediaChunks: [mediaChunk] } + }; + this.webSocketHandler.send(JSON.stringify(message)); + }); + } + + /** + * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead. + * + * Sends a stream of {@link GenerativeContentBlob}. + * + * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaStream( + mediaChunkStream: ReadableStream + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const reader = mediaChunkStream.getReader(); + while (true) { + try { + const { done, value } = await reader.read(); + + if (done) { + break; + } else if (!value) { + throw new Error('Missing chunk in reader, but reader is not done.'); + } + + await this.sendMediaChunks([value]); + } catch (e) { + // Re-throw any errors that occur during stream consumption or sending. + const message = + e instanceof Error ? e.message : 'Error processing media stream.'; + throw new AIError(AIErrorCode.REQUEST_ERROR, message); + } + } + } } diff --git a/packages/ai/src/models/generative-model.test.ts b/packages/ai/src/models/generative-model.test.ts index bcd78d746d4..90399e6811b 100644 --- a/packages/ai/src/models/generative-model.test.ts +++ b/packages/ai/src/models/generative-model.test.ts @@ -20,7 +20,8 @@ import { FunctionCallingMode, AI, InferenceMode, - AIErrorCode + AIErrorCode, + ChromeAdapter } from '../public-types'; import * as request from '../requests/request'; import { SinonStub, match, restore, stub } from 'sinon'; @@ -30,9 +31,9 @@ import { } from '../../test-utils/mock-response'; import sinonChai from 'sinon-chai'; import { VertexAIBackend } from '../backend'; -import { ChromeAdapterImpl } from '../methods/chrome-adapter'; import { AIError } from '../errors'; import chaiAsPromised from 'chai-as-promised'; +import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; use(sinonChai); use(chaiAsPromised); @@ -51,12 +52,6 @@ const fakeAI: AI = { location: 'us-central1' }; -const fakeChromeAdapter = new ChromeAdapterImpl( - // @ts-expect-error - undefined, - InferenceMode.PREFER_ON_DEVICE -); - describe('GenerativeModel', () => { it('passes params through to generateContent', async () => { const genModel = new GenerativeModel( @@ -436,7 +431,7 @@ describe('GenerativeModel', () => { describe('GenerativeModel dispatch logic', () => { let makeRequestStub: SinonStub; - let mockChromeAdapter: ChromeAdapterImpl; + let mockChromeAdapter: ChromeAdapter; function stubMakeRequest(stream?: boolean): void { if (stream) { diff --git a/packages/ai/src/models/live-generative-model.test.ts b/packages/ai/src/models/live-generative-model.test.ts index 495f340b846..a899e0b39fa 100644 --- a/packages/ai/src/models/live-generative-model.test.ts +++ b/packages/ai/src/models/live-generative-model.test.ts @@ -168,4 +168,35 @@ describe('LiveGenerativeModel', () => { mockHandler.simulateServerMessage({ setupComplete: true }); await connectPromise; }); + it('connect() should deconstruct generationConfig to send transcription configs in top level setup', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { + model: 'gemini-pro', + generationConfig: { + temperature: 0.8, + inputAudioTranscription: {}, + outputAudioTranscription: {} + }, + systemInstruction: { role: 'system', parts: [{ text: 'Be a pirate' }] } + }, + mockHandler + ); + const connectPromise = model.connect(); + + // Wait for setup message + await clock.runAllAsync(); + + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + // inputAudioTranscription and outputAudioTranscription should be at the top-level setup message, + // rather than in the generationConfig. + expect(sentData.setup.generationConfig).to.deep.equal({ temperature: 0.8 }); + expect(sentData.setup.inputAudioTranscription).to.deep.equal({}); + expect(sentData.setup.outputAudioTranscription).to.deep.equal({}); + expect(sentData.setup.systemInstruction.parts[0].text).to.equal( + 'Be a pirate' + ); + mockHandler.simulateServerMessage({ setupComplete: true }); + await connectPromise; + }); }); diff --git a/packages/ai/src/models/live-generative-model.ts b/packages/ai/src/models/live-generative-model.ts index 251df095202..a89921070e3 100644 --- a/packages/ai/src/models/live-generative-model.ts +++ b/packages/ai/src/models/live-generative-model.ts @@ -86,13 +86,23 @@ export class LiveGenerativeModel extends AIModel { fullModelPath = `projects/${this._apiSettings.project}/locations/${this._apiSettings.location}/${this.model}`; } + // inputAudioTranscription and outputAudioTranscription are on the generation config in the public API, + // but the backend expects them to be in the `setup` message. + const { + inputAudioTranscription, + outputAudioTranscription, + ...generationConfig + } = this.generationConfig; + const setupMessage: _LiveClientSetup = { setup: { model: fullModelPath, - generationConfig: this.generationConfig, + generationConfig, tools: this.tools, toolConfig: this.toolConfig, - systemInstruction: this.systemInstruction + systemInstruction: this.systemInstruction, + inputAudioTranscription, + outputAudioTranscription } }; diff --git a/packages/ai/src/requests/hybrid-helpers.test.ts b/packages/ai/src/requests/hybrid-helpers.test.ts index a758f34ad21..d5d13691316 100644 --- a/packages/ai/src/requests/hybrid-helpers.test.ts +++ b/packages/ai/src/requests/hybrid-helpers.test.ts @@ -18,17 +18,22 @@ import { use, expect } from 'chai'; import { SinonStub, SinonStubbedInstance, restore, stub } from 'sinon'; import { callCloudOrDevice } from './hybrid-helpers'; -import { GenerateContentRequest, InferenceMode, AIErrorCode } from '../types'; +import { + GenerateContentRequest, + InferenceMode, + AIErrorCode, + ChromeAdapter, + InferenceSource +} from '../types'; import { AIError } from '../errors'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; -import { ChromeAdapterImpl } from '../methods/chrome-adapter'; use(sinonChai); use(chaiAsPromised); describe('callCloudOrDevice', () => { - let chromeAdapter: SinonStubbedInstance; + let chromeAdapter: SinonStubbedInstance; let onDeviceCall: SinonStub; let inCloudCall: SinonStub; let request: GenerateContentRequest; @@ -58,7 +63,8 @@ describe('callCloudOrDevice', () => { onDeviceCall, inCloudCall ); - expect(result).to.equal('in-cloud-response'); + expect(result.response).to.equal('in-cloud-response'); + expect(result.inferenceSource).to.equal(InferenceSource.IN_CLOUD); expect(inCloudCall).to.have.been.calledOnce; expect(onDeviceCall).to.not.have.been.called; }); @@ -76,7 +82,8 @@ describe('callCloudOrDevice', () => { onDeviceCall, inCloudCall ); - expect(result).to.equal('on-device-response'); + expect(result.response).to.equal('on-device-response'); + expect(result.inferenceSource).to.equal(InferenceSource.ON_DEVICE); expect(onDeviceCall).to.have.been.calledOnce; expect(inCloudCall).to.not.have.been.called; }); @@ -89,7 +96,8 @@ describe('callCloudOrDevice', () => { onDeviceCall, inCloudCall ); - expect(result).to.equal('in-cloud-response'); + expect(result.response).to.equal('in-cloud-response'); + expect(result.inferenceSource).to.equal(InferenceSource.IN_CLOUD); expect(inCloudCall).to.have.been.calledOnce; expect(onDeviceCall).to.not.have.been.called; }); @@ -108,7 +116,8 @@ describe('callCloudOrDevice', () => { onDeviceCall, inCloudCall ); - expect(result).to.equal('on-device-response'); + expect(result.response).to.equal('on-device-response'); + expect(result.inferenceSource).to.equal(InferenceSource.ON_DEVICE); expect(onDeviceCall).to.have.been.calledOnce; expect(inCloudCall).to.not.have.been.called; }); @@ -136,7 +145,8 @@ describe('callCloudOrDevice', () => { onDeviceCall, inCloudCall ); - expect(result).to.equal('in-cloud-response'); + expect(result.response).to.equal('in-cloud-response'); + expect(result.inferenceSource).to.equal(InferenceSource.IN_CLOUD); expect(inCloudCall).to.have.been.calledOnce; expect(onDeviceCall).to.not.have.been.called; }); @@ -154,7 +164,8 @@ describe('callCloudOrDevice', () => { onDeviceCall, inCloudCall ); - expect(result).to.equal('in-cloud-response'); + expect(result.response).to.equal('in-cloud-response'); + expect(result.inferenceSource).to.equal(InferenceSource.IN_CLOUD); expect(inCloudCall).to.have.been.calledOnce; expect(onDeviceCall).to.not.have.been.called; }); @@ -169,7 +180,8 @@ describe('callCloudOrDevice', () => { onDeviceCall, inCloudCall ); - expect(result).to.equal('on-device-response'); + expect(result.response).to.equal('on-device-response'); + expect(result.inferenceSource).to.equal(InferenceSource.ON_DEVICE); expect(inCloudCall).to.have.been.calledOnce; expect(onDeviceCall).to.have.been.calledOnce; }); diff --git a/packages/ai/src/requests/hybrid-helpers.ts b/packages/ai/src/requests/hybrid-helpers.ts index 3140594c00e..2697216cd8a 100644 --- a/packages/ai/src/requests/hybrid-helpers.ts +++ b/packages/ai/src/requests/hybrid-helpers.ts @@ -20,9 +20,9 @@ import { GenerateContentRequest, InferenceMode, AIErrorCode, - ChromeAdapter + ChromeAdapter, + InferenceSource } from '../types'; -import { ChromeAdapterImpl } from '../methods/chrome-adapter'; const errorsCausingFallback: AIErrorCode[] = [ // most network errors @@ -33,6 +33,11 @@ const errorsCausingFallback: AIErrorCode[] = [ AIErrorCode.API_NOT_ENABLED ]; +interface CallResult { + response: Response; + inferenceSource: InferenceSource; +} + /** * Dispatches a request to the appropriate backend (on-device or in-cloud) * based on the inference mode. @@ -48,41 +53,60 @@ export async function callCloudOrDevice( chromeAdapter: ChromeAdapter | undefined, onDeviceCall: () => Promise, inCloudCall: () => Promise -): Promise { +): Promise> { if (!chromeAdapter) { - return inCloudCall(); + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; } - switch ((chromeAdapter as ChromeAdapterImpl).mode) { + switch (chromeAdapter.mode) { case InferenceMode.ONLY_ON_DEVICE: if (await chromeAdapter.isAvailable(request)) { - return onDeviceCall(); + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; } throw new AIError( AIErrorCode.UNSUPPORTED, 'Inference mode is ONLY_ON_DEVICE, but an on-device model is not available.' ); case InferenceMode.ONLY_IN_CLOUD: - return inCloudCall(); + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; case InferenceMode.PREFER_IN_CLOUD: try { - return await inCloudCall(); + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; } catch (e) { if (e instanceof AIError && errorsCausingFallback.includes(e.code)) { - return onDeviceCall(); + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; } throw e; } case InferenceMode.PREFER_ON_DEVICE: if (await chromeAdapter.isAvailable(request)) { - return onDeviceCall(); + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; } - return inCloudCall(); + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; default: throw new AIError( AIErrorCode.ERROR, - `Unexpected infererence mode: ${ - (chromeAdapter as ChromeAdapterImpl).mode - }` + `Unexpected infererence mode: ${chromeAdapter.mode}` ); } } diff --git a/packages/ai/src/requests/response-helpers.ts b/packages/ai/src/requests/response-helpers.ts index 930bfabb2ae..bb3748f6bc9 100644 --- a/packages/ai/src/requests/response-helpers.ts +++ b/packages/ai/src/requests/response-helpers.ts @@ -25,7 +25,8 @@ import { ImagenInlineImage, AIErrorCode, InlineDataPart, - Part + Part, + InferenceSource } from '../types'; import { AIError } from '../errors'; import { logger } from '../logger'; @@ -66,7 +67,8 @@ function hasValidCandidates(response: GenerateContentResponse): boolean { * other modifications that improve usability. */ export function createEnhancedContentResponse( - response: GenerateContentResponse + response: GenerateContentResponse, + inferenceSource: InferenceSource = InferenceSource.IN_CLOUD ): EnhancedGenerateContentResponse { /** * The Vertex AI backend omits default values. @@ -79,6 +81,7 @@ export function createEnhancedContentResponse( } const responseWithHelpers = addHelpers(response); + responseWithHelpers.inferenceSource = inferenceSource; return responseWithHelpers; } diff --git a/packages/ai/src/requests/stream-reader.test.ts b/packages/ai/src/requests/stream-reader.test.ts index 2e50bbb3d3e..ca3c2cdcfe2 100644 --- a/packages/ai/src/requests/stream-reader.test.ts +++ b/packages/ai/src/requests/stream-reader.test.ts @@ -34,7 +34,8 @@ import { HarmCategory, HarmProbability, SafetyRating, - AIErrorCode + AIErrorCode, + InferenceSource } from '../types'; import { AIError } from '../errors'; import { ApiSettings } from '../types/internal'; @@ -61,6 +62,7 @@ describe('getResponseStream', () => { .map(v => JSON.stringify(v)) .map(v => 'data: ' + v + '\r\n\r\n') .join('') + // @ts-ignore ).pipeThrough(new TextDecoderStream('utf8', { fatal: true })); const responseStream = getResponseStream<{ text: string }>(inputStream); const reader = responseStream.getReader(); @@ -88,9 +90,33 @@ describe('processStream', () => { const result = processStream(fakeResponse as Response, fakeApiSettings); for await (const response of result.stream) { expect(response.text()).to.not.be.empty; + expect(response.inferenceSource).to.equal(InferenceSource.IN_CLOUD); } const aggregatedResponse = await result.response; expect(aggregatedResponse.text()).to.include('Cheyenne'); + expect(aggregatedResponse.inferenceSource).to.equal( + InferenceSource.IN_CLOUD + ); + }); + it('streaming response - short - on-device', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-basic-reply-short.txt' + ); + const result = processStream( + fakeResponse as Response, + fakeApiSettings, + InferenceSource.ON_DEVICE + ); + for await (const response of result.stream) { + expect(response.text()).to.not.be.empty; + expect(response.inferenceSource).to.equal(InferenceSource.ON_DEVICE); + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.include('Cheyenne'); + expect(aggregatedResponse.inferenceSource).to.equal( + InferenceSource.ON_DEVICE + ); }); it('streaming response - long', async () => { const fakeResponse = getMockResponseStreaming( diff --git a/packages/ai/src/requests/stream-reader.ts b/packages/ai/src/requests/stream-reader.ts index 042c052fa82..b4968969be7 100644 --- a/packages/ai/src/requests/stream-reader.ts +++ b/packages/ai/src/requests/stream-reader.ts @@ -28,7 +28,11 @@ import { createEnhancedContentResponse } from './response-helpers'; import * as GoogleAIMapper from '../googleai-mappers'; import { GoogleAIGenerateContentResponse } from '../types/googleai'; import { ApiSettings } from '../types/internal'; -import { BackendType, URLContextMetadata } from '../public-types'; +import { + BackendType, + InferenceSource, + URLContextMetadata +} from '../public-types'; const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/; @@ -42,7 +46,8 @@ const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/; */ export function processStream( response: Response, - apiSettings: ApiSettings + apiSettings: ApiSettings, + inferenceSource?: InferenceSource ): GenerateContentStreamResult { const inputStream = response.body!.pipeThrough( new TextDecoderStream('utf8', { fatal: true }) @@ -51,14 +56,15 @@ export function processStream( getResponseStream(inputStream); const [stream1, stream2] = responseStream.tee(); return { - stream: generateResponseSequence(stream1, apiSettings), - response: getResponsePromise(stream2, apiSettings) + stream: generateResponseSequence(stream1, apiSettings, inferenceSource), + response: getResponsePromise(stream2, apiSettings, inferenceSource) }; } async function getResponsePromise( stream: ReadableStream, - apiSettings: ApiSettings + apiSettings: ApiSettings, + inferenceSource?: InferenceSource ): Promise { const allResponses: GenerateContentResponse[] = []; const reader = stream.getReader(); @@ -71,7 +77,10 @@ async function getResponsePromise( generateContentResponse as GoogleAIGenerateContentResponse ); } - return createEnhancedContentResponse(generateContentResponse); + return createEnhancedContentResponse( + generateContentResponse, + inferenceSource + ); } allResponses.push(value); @@ -80,7 +89,8 @@ async function getResponsePromise( async function* generateResponseSequence( stream: ReadableStream, - apiSettings: ApiSettings + apiSettings: ApiSettings, + inferenceSource?: InferenceSource ): AsyncGenerator { const reader = stream.getReader(); while (true) { @@ -94,10 +104,11 @@ async function* generateResponseSequence( enhancedResponse = createEnhancedContentResponse( GoogleAIMapper.mapGenerateContentResponse( value as GoogleAIGenerateContentResponse - ) + ), + inferenceSource ); } else { - enhancedResponse = createEnhancedContentResponse(value); + enhancedResponse = createEnhancedContentResponse(value, inferenceSource); } const firstCandidate = enhancedResponse.candidates?.[0]; diff --git a/packages/ai/src/service.ts b/packages/ai/src/service.ts index 0beb8dda1c3..a862f19422e 100644 --- a/packages/ai/src/service.ts +++ b/packages/ai/src/service.ts @@ -16,7 +16,13 @@ */ import { FirebaseApp, _FirebaseService } from '@firebase/app'; -import { AI, AIOptions, InferenceMode, OnDeviceParams } from './public-types'; +import { + AI, + AIOptions, + ChromeAdapter, + InferenceMode, + OnDeviceParams +} from './public-types'; import { AppCheckInternalComponentName, FirebaseAppCheckInternal @@ -27,7 +33,6 @@ import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { Backend, VertexAIBackend } from './backend'; -import { ChromeAdapterImpl } from './methods/chrome-adapter'; export class AIService implements AI, _FirebaseService { auth: FirebaseAuthInternal | null; @@ -44,7 +49,7 @@ export class AIService implements AI, _FirebaseService { mode: InferenceMode, window?: Window, params?: OnDeviceParams - ) => ChromeAdapterImpl | undefined + ) => ChromeAdapter | undefined ) { const appCheck = appCheckProvider?.getImmediate({ optional: true }); const auth = authProvider?.getImmediate({ optional: true }); diff --git a/packages/ai/src/types/chrome-adapter.ts b/packages/ai/src/types/chrome-adapter.ts index fc33325217f..a026b8a1a40 100644 --- a/packages/ai/src/types/chrome-adapter.ts +++ b/packages/ai/src/types/chrome-adapter.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { InferenceMode } from './enums'; import { CountTokensRequest, GenerateContentRequest } from './requests'; /** @@ -27,6 +28,10 @@ import { CountTokensRequest, GenerateContentRequest } from './requests'; * @beta */ export interface ChromeAdapter { + /** + * @internal + */ + mode: InferenceMode; /** * Checks if the on-device model is capable of handling a given * request. diff --git a/packages/ai/src/types/enums.ts b/packages/ai/src/types/enums.ts index cd7029df3b0..f7c55d5e4c3 100644 --- a/packages/ai/src/types/enums.ts +++ b/packages/ai/src/types/enums.ts @@ -379,6 +379,24 @@ export const InferenceMode = { */ export type InferenceMode = (typeof InferenceMode)[keyof typeof InferenceMode]; +/** + * Indicates whether inference happened on-device or in-cloud. + * + * @beta + */ +export const InferenceSource = { + 'ON_DEVICE': 'on_device', + 'IN_CLOUD': 'in_cloud' +} as const; + +/** + * Indicates whether inference happened on-device or in-cloud. + * + * @beta + */ +export type InferenceSource = + (typeof InferenceSource)[keyof typeof InferenceSource]; + /** * Represents the result of the code execution. * diff --git a/packages/ai/src/types/live-responses.ts b/packages/ai/src/types/live-responses.ts index d1870fa109f..3bdb32c1269 100644 --- a/packages/ai/src/types/live-responses.ts +++ b/packages/ai/src/types/live-responses.ts @@ -21,7 +21,13 @@ import { GenerativeContentBlob, Part } from './content'; -import { LiveGenerationConfig, Tool, ToolConfig } from './requests'; +import { + AudioTranscriptionConfig, + LiveGenerationConfig, + Tool, + ToolConfig +} from './requests'; +import { Transcription } from './responses'; /** * User input that is sent to the model. @@ -33,6 +39,8 @@ export interface _LiveClientContent { clientContent: { turns: [Content]; turnComplete: boolean; + inputTranscription?: Transcription; + outputTranscription?: Transcription; }; } @@ -44,7 +52,14 @@ export interface _LiveClientContent { // eslint-disable-next-line @typescript-eslint/naming-convention export interface _LiveClientRealtimeInput { realtimeInput: { - mediaChunks: GenerativeContentBlob[]; + text?: string; + audio?: GenerativeContentBlob; + video?: GenerativeContentBlob; + + /** + * @deprecated Use `text`, `audio`, and `video` instead. + */ + mediaChunks?: GenerativeContentBlob[]; }; } @@ -67,9 +82,22 @@ export interface _LiveClientToolResponse { export interface _LiveClientSetup { setup: { model: string; - generationConfig?: LiveGenerationConfig; + generationConfig?: _LiveGenerationConfig; tools?: Tool[]; toolConfig?: ToolConfig; systemInstruction?: string | Part | Content; + inputAudioTranscription?: AudioTranscriptionConfig; + outputAudioTranscription?: AudioTranscriptionConfig; }; } + +/** + * The Live Generation Config. + * + * The public API ({@link LiveGenerationConfig}) has `inputAudioTranscription` and `outputAudioTranscription`, + * but the server expects these fields to be in the top-level `setup` message. This was a conscious API decision. + */ +export type _LiveGenerationConfig = Omit< + LiveGenerationConfig, + 'inputAudioTranscription' | 'outputAudioTranscription' +>; diff --git a/packages/ai/src/types/requests.ts b/packages/ai/src/types/requests.ts index 1e5fa367420..6e5d2147686 100644 --- a/packages/ai/src/types/requests.ts +++ b/packages/ai/src/types/requests.ts @@ -184,6 +184,24 @@ export interface LiveGenerationConfig { * The modalities of the response. */ responseModalities?: ResponseModality[]; + /** + * Enables transcription of audio input. + * + * When enabled, the model will respond with transcriptions of your audio input in the `inputTranscriptions` property + * in {@link LiveServerContent} messages. Note that the transcriptions are broken up across + * messages, so you may only receive small amounts of text per message. For example, if you ask the model + * "How are you today?", the model may transcribe that input across three messages, broken up as "How a", "re yo", "u today?". + */ + inputAudioTranscription?: AudioTranscriptionConfig; + /** + * Enables transcription of audio input. + * + * When enabled, the model will respond with transcriptions of its audio output in the `outputTranscription` property + * in {@link LiveServerContent} messages. Note that the transcriptions are broken up across + * messages, so you may only receive small amounts of text per message. For example, if the model says + * "How are you today?", the model may transcribe that output across three messages, broken up as "How a", "re yo", "u today?". + */ + outputAudioTranscription?: AudioTranscriptionConfig; } /** @@ -478,3 +496,8 @@ export interface SpeechConfig { */ voiceConfig?: VoiceConfig; } + +/** + * The audio transcription configuration. + */ +export interface AudioTranscriptionConfig {} diff --git a/packages/ai/src/types/responses.ts b/packages/ai/src/types/responses.ts index 8b8e1351675..be56d0d2baa 100644 --- a/packages/ai/src/types/responses.ts +++ b/packages/ai/src/types/responses.ts @@ -22,6 +22,7 @@ import { HarmCategory, HarmProbability, HarmSeverity, + InferenceSource, Modality } from './enums'; @@ -88,6 +89,12 @@ export interface EnhancedGenerateContentResponse * set to `true`. */ thoughtSummary: () => string | undefined; + /** + * Indicates whether inference happened on-device or in-cloud. + * + * @beta + */ + inferenceSource?: InferenceSource; } /** @@ -546,6 +553,29 @@ export interface LiveServerContent { * model was not interrupted. */ interrupted?: boolean; + /** + * Transcription of the audio that was input to the model. + */ + inputTranscription?: Transcription; + /** + * Transcription of the audio output from the model. + */ + outputTranscription?: Transcription; +} + +/** + * Transcription of audio. This can be returned from a {@link LiveGenerativeModel} if transcription + * is enabled with the `inputAudioTranscription` or `outputAudioTranscription` properties on + * the {@link LiveGenerationConfig}. + * + * @beta + */ + +export interface Transcription { + /** + * The text transcription of the audio. + */ + text?: string; } /** diff --git a/packages/ai/test-utils/get-fake-firebase-services.ts b/packages/ai/test-utils/get-fake-firebase-services.ts index 63789c1a00e..f639d919ed0 100644 --- a/packages/ai/test-utils/get-fake-firebase-services.ts +++ b/packages/ai/test-utils/get-fake-firebase-services.ts @@ -21,10 +21,11 @@ import { _registerComponent, _addOrOverwriteComponent } from '@firebase/app'; -import { Component, ComponentType } from '@firebase/component'; +import { Component, ComponentType, InstanceFactory } from '@firebase/component'; import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types'; import { AI_TYPE } from '../src/constants'; -import { factory } from '../src/factory-browser'; +import { factory as factoryNode } from '../src/factory-node'; +import { ChromeAdapter, InferenceMode } from '../src/types'; const fakeConfig = { projectId: 'projectId', @@ -34,10 +35,13 @@ const fakeConfig = { storageBucket: 'storageBucket' }; -export function getFullApp(fakeAppParams?: { - appId?: string; - apiKey?: string; -}): FirebaseApp { +export function getFullApp( + fakeAppParams?: { + appId?: string; + apiKey?: string; + }, + factory: InstanceFactory<'AI'> = factoryNode +): FirebaseApp { _registerComponent( new Component(AI_TYPE, factory, ComponentType.PUBLIC).setMultipleInstances( true @@ -69,3 +73,12 @@ export function getFullApp(fakeAppParams?: { ); return app; } + +export const fakeChromeAdapter: ChromeAdapter = { + mode: InferenceMode.PREFER_ON_DEVICE, + // Individual tests may stub this to resolve true as needed. + isAvailable: () => Promise.resolve(false), + generateContent: () => Promise.resolve({} as Response), + generateContentStream: () => Promise.resolve({} as Response), + countTokens: () => Promise.resolve({} as Response) +}; diff --git a/packages/analytics-compat/package.json b/packages/analytics-compat/package.json index a4c07015feb..f06c82a5541 100644 --- a/packages/analytics-compat/package.json +++ b/packages/analytics-compat/package.json @@ -22,7 +22,7 @@ "@firebase/app-compat": "0.x" }, "devDependencies": { - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/analytics/package.json b/packages/analytics/package.json index b5dd7e377e7..66fb8b4230b 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -47,7 +47,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "rollup-plugin-dts": "5.3.1", "@rollup/plugin-commonjs": "21.1.0", diff --git a/packages/app-check-compat/package.json b/packages/app-check-compat/package.json index a2fc5b7b785..a510523e5ac 100644 --- a/packages/app-check-compat/package.json +++ b/packages/app-check-compat/package.json @@ -43,7 +43,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/app-check/package.json b/packages/app-check/package.json index 5700ea8934b..cb1ad6909e3 100644 --- a/packages/app-check/package.json +++ b/packages/app-check/package.json @@ -44,7 +44,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/app-compat/CHANGELOG.md b/packages/app-compat/CHANGELOG.md index 5d1f234c05f..822ae9c1cd7 100644 --- a/packages/app-compat/CHANGELOG.md +++ b/packages/app-compat/CHANGELOG.md @@ -1,5 +1,12 @@ # @firebase/app-compat +## 0.5.5 + +### Patch Changes + +- Updated dependencies []: + - @firebase/app@0.14.5 + ## 0.5.4 ### Patch Changes diff --git a/packages/app-compat/package.json b/packages/app-compat/package.json index 548e57cad5c..c1ad2752142 100644 --- a/packages/app-compat/package.json +++ b/packages/app-compat/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/app-compat", - "version": "0.5.4", + "version": "0.5.5", "description": "The primary entrypoint to the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -37,7 +37,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "@firebase/util": "1.13.0", "@firebase/logger": "0.5.0", "@firebase/component": "0.7.0", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index b72699ffe20..a370d88e795 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,11 @@ # @firebase/app +## 0.14.5 + +### Patch Changes + +- Update SDK_VERSION. + ## 0.14.4 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index aef76c7da8b..c695239580c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/app", - "version": "0.14.4", + "version": "0.14.5", "description": "The primary entrypoint to the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", diff --git a/packages/auth-compat/CHANGELOG.md b/packages/auth-compat/CHANGELOG.md index 401045ee12a..c8024cbe85d 100644 --- a/packages/auth-compat/CHANGELOG.md +++ b/packages/auth-compat/CHANGELOG.md @@ -1,5 +1,12 @@ # @firebase/auth-compat +## 0.6.1 + +### Patch Changes + +- Updated dependencies [[`91c218d`](https://github.com/firebase/firebase-js-sdk/commit/91c218db2d14cb4f1b978b9073510b8bc8f91233), [`2615081`](https://github.com/firebase/firebase-js-sdk/commit/261508183c249dcec737448dde3aad7399f4668c)]: + - @firebase/auth@1.11.1 + ## 0.6.0 ### Minor Changes diff --git a/packages/auth-compat/package.json b/packages/auth-compat/package.json index 0db3546ce95..47f326fd714 100644 --- a/packages/auth-compat/package.json +++ b/packages/auth-compat/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/auth-compat", - "version": "0.6.0", + "version": "0.6.1", "description": "FirebaseAuth compatibility package that uses API style compatible with Firebase@8 and prior versions", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", @@ -49,7 +49,7 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/auth": "1.11.0", + "@firebase/auth": "1.11.1", "@firebase/auth-types": "0.13.0", "@firebase/component": "0.7.0", "@firebase/util": "1.13.0", @@ -57,7 +57,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "@rollup/plugin-json": "6.1.0", "rollup": "2.79.2", "rollup-plugin-replace": "2.2.0", diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 6439b542480..298371600db 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,13 @@ # @firebase/auth +## 1.11.1 + +### Patch Changes + +- [`91c218d`](https://github.com/firebase/firebase-js-sdk/commit/91c218db2d14cb4f1b978b9073510b8bc8f91233) [#9313](https://github.com/firebase/firebase-js-sdk/pull/9313) - Expose `browserCookiePersistence` beta feature in public typings. + +- [`2615081`](https://github.com/firebase/firebase-js-sdk/commit/261508183c249dcec737448dde3aad7399f4668c) [#9297](https://github.com/firebase/firebase-js-sdk/pull/9297) (fixes [#9270](https://github.com/firebase/firebase-js-sdk/issues/9270)) - Export MISSING_PASSWORD via AuthErrorCodes in @firebase/auth. + ## 1.11.0 ### Minor Changes diff --git a/packages/auth/api-extractor.json b/packages/auth/api-extractor.json index 8b44b43f4d1..85e6f846d5c 100644 --- a/packages/auth/api-extractor.json +++ b/packages/auth/api-extractor.json @@ -4,6 +4,6 @@ "dtsRollup": { "enabled": true, "untrimmedFilePath": "/dist/.d.ts", - "publicTrimmedFilePath": "/dist/-public.d.ts" + "betaTrimmedFilePath": "/dist/-public.d.ts" } } diff --git a/packages/auth/package.json b/packages/auth/package.json index e21a6385dfb..c515d4a1937 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/auth", - "version": "1.11.0", + "version": "1.11.1", "description": "The Firebase Authenticaton component of the Firebase JS SDK.", "author": "Firebase (https://firebase.google.com/)", "main": "dist/node/index.js", @@ -131,7 +131,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-strip": "2.1.0", "@types/express": "4.17.21", diff --git a/packages/auth/src/core/errors.ts b/packages/auth/src/core/errors.ts index 0e7fb53059c..12c7c87c2b1 100644 --- a/packages/auth/src/core/errors.ts +++ b/packages/auth/src/core/errors.ts @@ -560,6 +560,7 @@ export const AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY = { MISSING_MFA_INFO: 'auth/missing-multi-factor-info', MISSING_MFA_SESSION: 'auth/missing-multi-factor-session', MISSING_PHONE_NUMBER: 'auth/missing-phone-number', + MISSING_PASSWORD: 'auth/missing-password', MISSING_SESSION_INFO: 'auth/missing-verification-id', MODULE_DESTROYED: 'auth/app-deleted', NEED_CONFIRMATION: 'auth/account-exists-with-different-credential', diff --git a/packages/data-connect/package.json b/packages/data-connect/package.json index 18dbb00e43d..bf1fc8ce332 100644 --- a/packages/data-connect/package.json +++ b/packages/data-connect/package.json @@ -55,7 +55,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/packages/data-connect/src/api.browser.ts b/packages/data-connect/src/api.browser.ts index 1ffcb8d1647..d31c3253537 100644 --- a/packages/data-connect/src/api.browser.ts +++ b/packages/data-connect/src/api.browser.ts @@ -102,6 +102,7 @@ export function subscribe( return ref.dataConnect._queryManager.addSubscription( ref, onResult, + onComplete, onError, initialCache ); diff --git a/packages/data-connect/src/api/DataConnect.ts b/packages/data-connect/src/api/DataConnect.ts index c25a09039ac..ced4ddf05f9 100644 --- a/packages/data-connect/src/api/DataConnect.ts +++ b/packages/data-connect/src/api/DataConnect.ts @@ -194,6 +194,7 @@ export class DataConnect { // @internal enableEmulator(transportOptions: TransportOptions): void { if ( + this._transportOptions && this._initialized && !areTransportOptionsEqual(this._transportOptions, transportOptions) ) { @@ -309,7 +310,10 @@ export function validateDCOptions(dcOptions: ConnectorConfig): boolean { throw new DataConnectError(Code.INVALID_ARGUMENT, 'DC Option Required'); } fields.forEach(field => { - if (dcOptions[field] === null || dcOptions[field] === undefined) { + if ( + dcOptions[field as keyof ConnectorConfig] === null || + dcOptions[field as keyof ConnectorConfig] === undefined + ) { throw new DataConnectError(Code.INVALID_ARGUMENT, `${field} Required`); } }); diff --git a/packages/data-connect/src/api/query.ts b/packages/data-connect/src/api/query.ts index a1cd0726160..43683cafd63 100644 --- a/packages/data-connect/src/api/query.ts +++ b/packages/data-connect/src/api/query.ts @@ -39,14 +39,6 @@ export type OnErrorSubscription = (err?: DataConnectError) => void; * Signature for unsubscribe from `subscribe` */ export type QueryUnsubscribe = () => void; -/** - * Representation of user provided subscription options. - */ -export interface DataConnectSubscription { - userCallback: OnResultSubscription; - errCallback?: (e?: DataConnectError) => void; - unsubscribe: () => void; -} /** * QueryRef object @@ -124,7 +116,7 @@ export function queryRef( dataConnect: dcInstance, refType: QUERY_STR, name: queryName, - variables + variables: variables as Variables }; } /** diff --git a/packages/data-connect/src/core/AppCheckTokenProvider.ts b/packages/data-connect/src/core/AppCheckTokenProvider.ts index 4b49a8f674a..615b45f3891 100644 --- a/packages/data-connect/src/core/AppCheckTokenProvider.ts +++ b/packages/data-connect/src/core/AppCheckTokenProvider.ts @@ -29,7 +29,7 @@ import { Provider } from '@firebase/component'; * Abstraction around AppCheck's token fetching capabilities. */ export class AppCheckTokenProvider { - private appCheck?: FirebaseAppCheckInternal; + private appCheck?: FirebaseAppCheckInternal | null; private serverAppAppCheckToken?: string; constructor( app: FirebaseApp, @@ -47,13 +47,13 @@ export class AppCheckTokenProvider { } } - getToken(): Promise { + getToken(): Promise { if (this.serverAppAppCheckToken) { return Promise.resolve({ token: this.serverAppAppCheckToken }); } if (!this.appCheck) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { // Support delayed initialization of FirebaseAppCheck. This allows our // customers to initialize the RTDB SDK before initializing Firebase // AppCheck and ensures that all requests are authenticated if a token diff --git a/packages/data-connect/src/core/QueryManager.ts b/packages/data-connect/src/core/QueryManager.ts index 8b7c59aea85..109f1d105b4 100644 --- a/packages/data-connect/src/core/QueryManager.ts +++ b/packages/data-connect/src/core/QueryManager.ts @@ -16,7 +16,7 @@ */ import { - DataConnectSubscription, + OnCompleteSubscription, OnErrorSubscription, OnResultSubscription, QueryPromise, @@ -39,6 +39,16 @@ import { setIfNotExists } from '../util/map'; import { Code, DataConnectError } from './error'; +/** + * Representation of user provided subscription options. + */ +interface DataConnectSubscription { + userCallback: OnResultSubscription; + errCallback?: (e?: DataConnectError) => void; + onCompleteCallback?: () => void; + unsubscribe: () => void; +} + interface TrackedQuery { ref: Omit, 'dataConnect'>; subscriptions: Array>; @@ -97,6 +107,7 @@ export class QueryManager { addSubscription( queryRef: OperationRef, onResultCallback: OnResultSubscription, + onCompleteCallback?: OnCompleteSubscription, onErrorCallback?: OnErrorSubscription, initialCache?: OpResult ): () => void { @@ -111,6 +122,7 @@ export class QueryManager { >; const subscription = { userCallback: onResultCallback, + onCompleteCallback, errCallback: onErrorCallback }; const unsubscribe = (): void => { @@ -118,6 +130,7 @@ export class QueryManager { trackedQuery.subscriptions = trackedQuery.subscriptions.filter( sub => sub !== subscription ); + onCompleteCallback?.(); }; if (initialCache && trackedQuery.currentCache !== initialCache) { logDebug('Initial cache found. Comparing dates.'); diff --git a/packages/data-connect/src/network/fetch.ts b/packages/data-connect/src/network/fetch.ts index 3e8e2cab476..62cef6fb220 100644 --- a/packages/data-connect/src/network/fetch.ts +++ b/packages/data-connect/src/network/fetch.ts @@ -56,9 +56,9 @@ export function dcFetch( url: string, body: DataConnectFetchBody, { signal }: AbortController, - appId: string | null, + appId: string | null | undefined, accessToken: string | null, - appCheckToken: string | null, + appCheckToken: string | null | undefined, _isUsingGen: boolean, _callerSdkType: CallerSdkType, _isUsingEmulator: boolean @@ -135,7 +135,7 @@ interface MessageObject { message?: string; } function getMessage(obj: MessageObject): string { - if ('message' in obj) { + if ('message' in obj && obj.message) { return obj.message; } return JSON.stringify(obj); diff --git a/packages/data-connect/src/network/transport/rest.ts b/packages/data-connect/src/network/transport/rest.ts index f16154dcb2a..4a3af8ac41a 100644 --- a/packages/data-connect/src/network/transport/rest.ts +++ b/packages/data-connect/src/network/transport/rest.ts @@ -34,7 +34,7 @@ export class RESTTransport implements DataConnectTransport { private _project = 'p'; private _serviceName: string; private _accessToken: string | null = null; - private _appCheckToken: string | null = null; + private _appCheckToken: string | null | undefined = null; private _lastToken: string | null = null; private _isUsingEmulator = false; constructor( @@ -106,7 +106,7 @@ export class RESTTransport implements DataConnectTransport { this._accessToken = newToken; } - async getWithAuth(forceToken = false): Promise { + async getWithAuth(forceToken = false): Promise { let starterPromise: Promise = new Promise(resolve => resolve(this._accessToken) ); diff --git a/packages/data-connect/src/util/validateArgs.ts b/packages/data-connect/src/util/validateArgs.ts index 15d1effa3da..e957786aa48 100644 --- a/packages/data-connect/src/util/validateArgs.ts +++ b/packages/data-connect/src/util/validateArgs.ts @@ -46,7 +46,7 @@ export function validateArgs( let realVars: Variables; if (dcOrVars && 'enableEmulator' in dcOrVars) { dcInstance = dcOrVars as DataConnect; - realVars = vars; + realVars = vars as Variables; } else { dcInstance = getDataConnect(connectorConfig); realVars = dcOrVars as Variables; diff --git a/packages/data-connect/test/unit/queries.test.ts b/packages/data-connect/test/unit/queries.test.ts index 68bd96268a6..02d19bf856e 100644 --- a/packages/data-connect/test/unit/queries.test.ts +++ b/packages/data-connect/test/unit/queries.test.ts @@ -20,15 +20,18 @@ import { expect } from 'chai'; import * as chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; -import { DataConnectOptions } from '../../src'; +import { DataConnectOptions, QueryRef, queryRef, subscribe } from '../../src'; import { AuthTokenListener, AuthTokenProvider } from '../../src/core/FirebaseAuthProvider'; import { initializeFetch } from '../../src/network/fetch'; import { RESTTransport } from '../../src/network/transport/rest'; +import { initDatabase } from '../util'; chai.use(chaiAsPromised); +chai.use(sinonChai); const options: DataConnectOptions = { connector: 'c', location: 'l', @@ -61,10 +64,103 @@ const fakeFetchImpl = sinon.stub().returns( status: 401 } as Response) ); +interface PostVariables { + testId: string; +} +const TEST_ID = crypto.randomUUID(); +interface PostListResponse { + posts: Post[]; +} +interface Post { + id: string; + description: string; +} +function getPostsRef(): QueryRef { + const dc = initDatabase(); + return queryRef(dc, 'ListPosts', { + testId: TEST_ID + }); +} describe('Queries', () => { afterEach(() => { fakeFetchImpl.resetHistory(); }); + it('should call onComplete callback after subscribe is called', async () => { + const taskListQuery = getPostsRef(); + const onCompleteUserStub = sinon.stub(); + const unsubscribe = subscribe(taskListQuery, { + onNext: () => {}, + onComplete: onCompleteUserStub + }); + expect(onCompleteUserStub).to.not.have.been.called; + unsubscribe(); + expect(onCompleteUserStub).to.have.been.calledOnce; + }); + it('should call onErr callback after a 401 occurs', async () => { + const json = {}; + const throwErrorFakeImpl = sinon.stub().returns( + Promise.resolve({ + json: () => { + return Promise.resolve(json); + }, + status: 401 + } as Response) + ); + initializeFetch(throwErrorFakeImpl); + const taskListQuery = getPostsRef(); + const onErrStub = sinon.stub(); + let unsubscribeFn: (() => void) | null = null; + const promise = new Promise((resolve, reject) => { + unsubscribeFn = subscribe(taskListQuery, { + onNext: () => { + resolve(null); + }, + onComplete: () => {}, + onErr: err => { + onErrStub(); + reject(err); + } + }); + }); + expect(onErrStub).not.to.have.been.called; + await expect(promise).to.have.eventually.been.rejected; + expect(onErrStub).to.have.been.calledOnce; + unsubscribeFn!(); + }); + it('should call onErr callback after a graphql error occurs', async () => { + const json = { + errors: [{ something: 'abc' }] + }; + const throwErrorFakeImpl = sinon.stub().returns( + Promise.resolve({ + json: () => { + return Promise.resolve(json); + }, + status: 200 + } as Response) + ); + initializeFetch(throwErrorFakeImpl); + const taskListQuery = getPostsRef(); + const onErrStub = sinon.stub(); + let unsubscribeFn: (() => void) | null = null; + const promise = new Promise((resolve, reject) => { + unsubscribeFn = subscribe(taskListQuery, { + onNext: () => { + resolve(null); + }, + onComplete: () => {}, + onErr: err => { + onErrStub(); + reject(err); + } + }); + }); + expect(onErrStub).not.to.have.been.called; + await expect(promise).to.have.eventually.been.rejected; + expect(onErrStub).to.have.been.calledOnce; + unsubscribeFn!(); + initializeFetch(globalThis.fetch); + }); it('[QUERY] should retry auth whenever the fetcher returns with unauthorized', async () => { initializeFetch(fakeFetchImpl); const authProvider = new FakeAuthProvider(); diff --git a/packages/data-connect/tsconfig.json b/packages/data-connect/tsconfig.json index 58561f50f5d..f97ac9fe899 100644 --- a/packages/data-connect/tsconfig.json +++ b/packages/data-connect/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../config/tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "strict": false + "strict": true }, "exclude": ["dist/**/*", "test/**/*"] } diff --git a/packages/database-compat/package.json b/packages/database-compat/package.json index 8fd65a1efef..1f4e0f55b71 100644 --- a/packages/database-compat/package.json +++ b/packages/database-compat/package.json @@ -57,7 +57,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "typescript": "5.5.4" }, "repository": { diff --git a/packages/database/package.json b/packages/database/package.json index 411a6bcfcdc..4d7f3d18dbc 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -57,7 +57,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/packages/firebase/CHANGELOG.md b/packages/firebase/CHANGELOG.md index 622cfef4d90..eb85a4d474e 100644 --- a/packages/firebase/CHANGELOG.md +++ b/packages/firebase/CHANGELOG.md @@ -1,5 +1,27 @@ # firebase +## 12.5.0 + +### Minor Changes + +- [`22e0a1a`](https://github.com/firebase/firebase-js-sdk/commit/22e0a1adbc994196690bd020472d119c1a3d200b) [#9291](https://github.com/firebase/firebase-js-sdk/pull/9291) - Deprecate `sendMediaChunks()` and `sendMediaStream()`. Instead, use the new methods added to the `LiveSession` class. + Add `sendTextRealtime()`, `sendAudioReatime()`, and `sendVideoRealtime()` to the `LiveSession` class. + +- [`bc5a7c4`](https://github.com/firebase/firebase-js-sdk/commit/bc5a7c4a74e72e9218d1435bfe50711c77b47cbd) [#9330](https://github.com/firebase/firebase-js-sdk/pull/9330) - Add support for audio transcriptions in the Live API. + +- [`c8263c4`](https://github.com/firebase/firebase-js-sdk/commit/c8263c471db4df1b0e23f0d2a11c69fd6b920e2e) [#9315](https://github.com/firebase/firebase-js-sdk/pull/9315) - Add `inferenceSource` to the response from `generateContent` and `generateContentStream`. This property indicates whether on-device or in-cloud inference was used to generate the result. + +### Patch Changes + +- [`2615081`](https://github.com/firebase/firebase-js-sdk/commit/261508183c249dcec737448dde3aad7399f4668c) [#9297](https://github.com/firebase/firebase-js-sdk/pull/9297) (fixes [#9270](https://github.com/firebase/firebase-js-sdk/issues/9270)) - Export MISSING_PASSWORD via AuthErrorCodes in @firebase/auth. + +- Updated dependencies [[`91c218d`](https://github.com/firebase/firebase-js-sdk/commit/91c218db2d14cb4f1b978b9073510b8bc8f91233), [`22e0a1a`](https://github.com/firebase/firebase-js-sdk/commit/22e0a1adbc994196690bd020472d119c1a3d200b), [`bc5a7c4`](https://github.com/firebase/firebase-js-sdk/commit/bc5a7c4a74e72e9218d1435bfe50711c77b47cbd), [`2615081`](https://github.com/firebase/firebase-js-sdk/commit/261508183c249dcec737448dde3aad7399f4668c), [`44d9891`](https://github.com/firebase/firebase-js-sdk/commit/44d9891f93298ab4bcef5170c40c235831af0276), [`c8263c4`](https://github.com/firebase/firebase-js-sdk/commit/c8263c471db4df1b0e23f0d2a11c69fd6b920e2e)]: + - @firebase/auth@1.11.1 + - @firebase/app@0.14.5 + - @firebase/ai@2.5.0 + - @firebase/auth-compat@0.6.1 + - @firebase/app-compat@0.5.5 + ## 12.4.0 ### Minor Changes diff --git a/packages/firebase/firestore/pipelines/index.cdn.ts b/packages/firebase/firestore/pipelines/index.cdn.ts new file mode 100644 index 00000000000..81e81b39d81 --- /dev/null +++ b/packages/firebase/firestore/pipelines/index.cdn.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from '@firebase/firestore'; + +import * as pipelines from '@firebase/firestore/pipelines'; +export { pipelines }; diff --git a/packages/firebase/gulpfile.js b/packages/firebase/gulpfile.js index 4ea24182af2..e05345fbdf7 100644 --- a/packages/firebase/gulpfile.js +++ b/packages/firebase/gulpfile.js @@ -21,7 +21,7 @@ const replace = require('gulp-replace'); const pkgJson = require('./package.json'); const files = pkgJson.components.map(component => { - const componentName = component.replace('/', '-'); + const componentName = component.replaceAll('/', '-'); return `firebase-${componentName}.js`; }); const FIREBASE_APP_URL = `https://www.gstatic.com/firebasejs/${pkgJson.version}/firebase-app.js`; diff --git a/packages/firebase/package.json b/packages/firebase/package.json index 72eebabc315..a68b9583c0c 100644 --- a/packages/firebase/package.json +++ b/packages/firebase/package.json @@ -1,6 +1,6 @@ { "name": "firebase", - "version": "12.4.0", + "version": "12.5.0", "description": "Firebase JavaScript library for web and Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -423,12 +423,12 @@ "trusted-type-check": "tsec -p tsconfig.json --noEmit" }, "dependencies": { - "@firebase/ai": "2.4.0", - "@firebase/app": "0.14.4", - "@firebase/app-compat": "0.5.4", + "@firebase/ai": "2.5.0", + "@firebase/app": "0.14.5", + "@firebase/app-compat": "0.5.5", "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.11.0", - "@firebase/auth-compat": "0.6.0", + "@firebase/auth": "1.11.1", + "@firebase/auth-compat": "0.6.1", "@firebase/data-connect": "0.3.11", "@firebase/database": "1.1.0", "@firebase/database-compat": "2.1.0", @@ -476,8 +476,8 @@ "auth/web-extension", "functions", "firestore", - "firestore/pipelines", "firestore/lite", + "firestore/pipelines", "firestore/lite/pipelines", "installations", "storage", diff --git a/packages/firebase/rollup.config.js b/packages/firebase/rollup.config.js index f96ff01666c..87b9f7c834d 100644 --- a/packages/firebase/rollup.config.js +++ b/packages/firebase/rollup.config.js @@ -20,6 +20,7 @@ import commonjs from '@rollup/plugin-commonjs'; import json from '@rollup/plugin-json'; import pkg from './package.json'; import { resolve } from 'path'; +import { existsSync } from 'fs'; import resolveModule from '@rollup/plugin-node-resolve'; import rollupTypescriptPlugin from 'rollup-plugin-typescript2'; import sourcemaps from 'rollup-plugin-sourcemaps'; @@ -149,10 +150,12 @@ const cdnBuilds = [ .map(component => { // It is needed for handling sub modules, for example firestore/lite which should produce firebase-firestore-lite.js // Otherwise, we will create a directory with '/' in the name. - const componentName = component.replace('/', '-'); + const componentName = component.replaceAll('/', '-'); return { - input: `${component}/index.ts`, + input: existsSync(`${component}/index.cdn.ts`) + ? `${component}/index.cdn.ts` + : `${component}/index.ts`, output: { file: `firebase-${componentName}.js`, sourcemap: true, diff --git a/packages/firestore-compat/package.json b/packages/firestore-compat/package.json index be610b3b7d5..07d80f462e1 100644 --- a/packages/firestore-compat/package.json +++ b/packages/firestore-compat/package.json @@ -53,7 +53,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "@types/eslint": "7.29.0", "rollup": "2.79.2", "rollup-plugin-sourcemaps": "0.6.3", diff --git a/packages/firestore/lite/pipelines/pipelines.ts b/packages/firestore/lite/pipelines/pipelines.ts index 1e5195c8e8c..9e4f8047014 100644 --- a/packages/firestore/lite/pipelines/pipelines.ts +++ b/packages/firestore/lite/pipelines/pipelines.ts @@ -102,10 +102,7 @@ export { isError, or, divide, - isNotNan, map, - isNotNull, - isNull, mod, documentId, equal, @@ -128,7 +125,6 @@ export { logicalMaximum, logicalMinimum, exists, - isNan, reverse, byteLength, charLength, @@ -160,6 +156,30 @@ export { timestampSubtract, ascending, descending, + arrayGet, + abs, + sum, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + log10, + concat, + currentTimestamp, + ifAbsent, + join, + length, + arraySum, + split, + timestampTruncate, + type, AliasedExpression, Field, Constant, @@ -169,5 +189,6 @@ export { AliasedAggregate, Selectable, BooleanExpression, - AggregateFunction + AggregateFunction, + TimeGranularity } from '../../src/lite-api/expressions'; diff --git a/packages/firestore/package.json b/packages/firestore/package.json index c660ee12b28..e92c13af49e 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -145,9 +145,9 @@ "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.14.4", - "@firebase/app-compat": "0.5.4", - "@firebase/auth": "1.11.0", + "@firebase/app": "0.14.5", + "@firebase/app-compat": "0.5.5", + "@firebase/auth": "1.11.1", "@rollup/plugin-alias": "5.1.1", "@rollup/plugin-json": "6.1.0", "@types/eslint": "7.29.0", diff --git a/packages/firestore/rollup.config.js b/packages/firestore/rollup.config.js index c9222f69ab4..b9db3922098 100644 --- a/packages/firestore/rollup.config.js +++ b/packages/firestore/rollup.config.js @@ -59,6 +59,35 @@ const browserPlugins = [ terser(util.manglePrivatePropertiesOptions) ]; +// TODO - update the implementation to match all content in the declare module block. +function declareModuleReplacePlugin() { + // The regex we created earlier + const moduleToReplace = + /declare module '\.\/\S+' \{\s+interface Firestore \{\s+pipeline\(\): PipelineSource;\s+}\s*}/gm; + + // What to replace it with (an empty string to remove it) + const replacement = + 'interface Firestore {pipeline(): PipelineSource;}'; + + return { + name: 'declare-module-replace', + generateBundle(options, bundle) { + const outputFileName = 'global_index.d.ts'; + if (!bundle[outputFileName]) { + console.warn( + `[regexReplacePlugin] File not found in bundle: ${outputFileName}` + ); + return; + } + + const chunk = bundle[outputFileName]; + if (chunk.type === 'chunk') { + chunk.code = chunk.code.replace(moduleToReplace, replacement); + } + } + }; +} + const allBuilds = [ // Intermediate Node ESM build without build target reporting // this is an intermediate build used to generate the actual esm and cjs builds @@ -214,7 +243,7 @@ const allBuilds = [ } }, { - input: 'dist/firestore/src/index.d.ts', + input: 'dist/firestore/src/global.d.ts', output: { file: 'dist/firestore/src/global_index.d.ts', format: 'es' @@ -222,7 +251,9 @@ const allBuilds = [ plugins: [ dts({ respectExternal: true - }) + }), + + declareModuleReplacePlugin() ] } ]; diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index a2feb19507f..f894ddce03e 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -302,6 +302,7 @@ export function configureFirestore(firestore: Firestore): void { firestore._databaseId, firestore._app?.options.appId || '', firestore._persistenceKey, + firestore._app?.options.apiKey, settings ); if (!firestore._componentsProvider) { diff --git a/packages/firestore/src/api/pipeline_impl.ts b/packages/firestore/src/api/pipeline_impl.ts index 843a3696f71..27f52e902b6 100644 --- a/packages/firestore/src/api/pipeline_impl.ts +++ b/packages/firestore/src/api/pipeline_impl.ts @@ -38,12 +38,22 @@ import { DocumentReference } from './reference'; import { ExpUserDataWriter } from './user_data_writer'; declare module './database' { + /** + * @beta + * Creates and returns a new PipelineSource, which allows specifying the source stage of a {@link Pipeline}. + * + * @example + * ``` + * let myPipeline: Pipeline = firestore.pipeline().collection('books'); + * ``` + */ interface Firestore { pipeline(): PipelineSource; } } /** + * @beta * Executes this pipeline and returns a Promise to represent the asynchronous operation. * * The returned Promise can be used to track the progress of the pipeline execution @@ -144,6 +154,15 @@ export function execute( ); } +/** + * @beta + * Creates and returns a new PipelineSource, which allows specifying the source stage of a {@link Pipeline}. + * + * @example + * ``` + * let myPipeline: Pipeline = firestore.pipeline().collection('books'); + * ``` + */ // Augment the Firestore class with the pipeline() factory method Firestore.prototype.pipeline = function (): PipelineSource { const userDataReader = newUserDataReader(this); diff --git a/packages/firestore/src/api_pipelines.ts b/packages/firestore/src/api_pipelines.ts index 057695ab5f4..693b7f54687 100644 --- a/packages/firestore/src/api_pipelines.ts +++ b/packages/firestore/src/api_pipelines.ts @@ -53,24 +53,6 @@ export { SortStageOptions } from './lite-api/stage_options'; -export { - Stage, - AddFields, - Aggregate, - Distinct, - CollectionSource, - CollectionGroupSource, - DatabaseSource, - DocumentsSource, - Where, - FindNearest, - Limit, - Offset, - Select, - Sort, - RawStage -} from './lite-api/stage'; - export { field, constant, @@ -98,7 +80,6 @@ export { logicalMaximum, logicalMinimum, exists, - isNan, reverse, byteLength, charLength, @@ -141,9 +122,6 @@ export { isError, ifError, isAbsent, - isNull, - isNotNull, - isNotNan, map, mapRemove, mapMerge, @@ -160,28 +138,28 @@ export { log, sqrt, stringReverse, - length as len, + length, abs, concat, currentTimestamp, - error, ifAbsent, join, log10, arraySum, + timestampTruncate, + split, + type, Expression, AliasedExpression, Field, FunctionExpression, Ordering, BooleanExpression, - AggregateFunction -} from './lite-api/expressions'; - -export type { + AggregateFunction, ExpressionType, AliasedAggregate, - Selectable + Selectable, + TimeGranularity } from './lite-api/expressions'; export { _internalPipelineToExecutePipelineRequestProto } from './remote/internal_serializer'; diff --git a/packages/firestore/src/core/database_info.ts b/packages/firestore/src/core/database_info.ts index a057516763f..ec75ba2486f 100644 --- a/packages/firestore/src/core/database_info.ts +++ b/packages/firestore/src/core/database_info.ts @@ -49,7 +49,8 @@ export class DatabaseInfo { readonly autoDetectLongPolling: boolean, readonly longPollingOptions: ExperimentalLongPollingOptions, readonly useFetchStreams: boolean, - readonly isUsingEmulator: boolean + readonly isUsingEmulator: boolean, + readonly apiKey: string | undefined ) {} } diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 009e7b2aba2..124ab4eaa44 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -146,7 +146,11 @@ export class FirestoreClient { * an async I/O to complete). */ public asyncQueue: AsyncQueue, - private databaseInfo: DatabaseInfo, + /** + * @internal + * Exposed for testing + */ + public _databaseInfo: DatabaseInfo, componentProvider?: { _offline: OfflineComponentProvider; _online: OnlineComponentProvider; @@ -167,7 +171,7 @@ export class FirestoreClient { get configuration(): ComponentConfiguration { return { asyncQueue: this.asyncQueue, - databaseInfo: this.databaseInfo, + databaseInfo: this._databaseInfo, clientId: this.clientId, authCredentials: this.authCredentials, appCheckCredentials: this.appCheckCredentials, diff --git a/packages/firestore/src/core/pipeline-util.ts b/packages/firestore/src/core/pipeline-util.ts index 3cf754e46f8..b27babf6951 100644 --- a/packages/firestore/src/core/pipeline-util.ts +++ b/packages/firestore/src/core/pipeline-util.ts @@ -28,7 +28,6 @@ import { } from '../lite-api/expressions'; import { Pipeline } from '../lite-api/pipeline'; import { doc } from '../lite-api/reference'; -import { isNanValue, isNullValue } from '../model/values'; import { fail } from '../util/assert'; import { Bound } from './bound'; @@ -53,90 +52,76 @@ import { export function toPipelineBooleanExpr(f: FilterInternal): BooleanExpression { if (f instanceof FieldFilterInternal) { const fieldValue = field(f.field.toString()); - if (isNanValue(f.value)) { - if (f.op === Operator.EQUAL) { - return and(fieldValue.exists(), fieldValue.isNan()); - } else { - return and(fieldValue.exists(), fieldValue.isNotNan()); + // Comparison filters + const value = f.value; + switch (f.op) { + case Operator.LESS_THAN: + return and( + fieldValue.exists(), + fieldValue.lessThan(Constant._fromProto(value)) + ); + case Operator.LESS_THAN_OR_EQUAL: + return and( + fieldValue.exists(), + fieldValue.lessThanOrEqual(Constant._fromProto(value)) + ); + case Operator.GREATER_THAN: + return and( + fieldValue.exists(), + fieldValue.greaterThan(Constant._fromProto(value)) + ); + case Operator.GREATER_THAN_OR_EQUAL: + return and( + fieldValue.exists(), + fieldValue.greaterThanOrEqual(Constant._fromProto(value)) + ); + case Operator.EQUAL: + return and( + fieldValue.exists(), + fieldValue.equal(Constant._fromProto(value)) + ); + case Operator.NOT_EQUAL: + return and( + fieldValue.exists(), + fieldValue.notEqual(Constant._fromProto(value)) + ); + case Operator.ARRAY_CONTAINS: + return and( + fieldValue.exists(), + fieldValue.arrayContains(Constant._fromProto(value)) + ); + case Operator.IN: { + const values = value?.arrayValue?.values?.map((val: any) => + Constant._fromProto(val) + ); + if (!values) { + return and(fieldValue.exists(), fieldValue.equalAny([])); + } else if (values.length === 1) { + return and(fieldValue.exists(), fieldValue.equal(values[0])); + } else { + return and(fieldValue.exists(), fieldValue.equalAny(values)); + } } - } else if (isNullValue(f.value)) { - if (f.op === Operator.EQUAL) { - return and(fieldValue.exists(), fieldValue.isNull()); - } else { - return and(fieldValue.exists(), fieldValue.isNotNull()); + case Operator.ARRAY_CONTAINS_ANY: { + const values = value?.arrayValue?.values?.map((val: any) => + Constant._fromProto(val) + ); + return and(fieldValue.exists(), fieldValue.arrayContainsAny(values!)); } - } else { - // Comparison filters - const value = f.value; - switch (f.op) { - case Operator.LESS_THAN: - return and( - fieldValue.exists(), - fieldValue.lessThan(Constant._fromProto(value)) - ); - case Operator.LESS_THAN_OR_EQUAL: - return and( - fieldValue.exists(), - fieldValue.lessThanOrEqual(Constant._fromProto(value)) - ); - case Operator.GREATER_THAN: - return and( - fieldValue.exists(), - fieldValue.greaterThan(Constant._fromProto(value)) - ); - case Operator.GREATER_THAN_OR_EQUAL: - return and( - fieldValue.exists(), - fieldValue.greaterThanOrEqual(Constant._fromProto(value)) - ); - case Operator.EQUAL: - return and( - fieldValue.exists(), - fieldValue.equal(Constant._fromProto(value)) - ); - case Operator.NOT_EQUAL: - return and( - fieldValue.exists(), - fieldValue.notEqual(Constant._fromProto(value)) - ); - case Operator.ARRAY_CONTAINS: - return and( - fieldValue.exists(), - fieldValue.arrayContains(Constant._fromProto(value)) - ); - case Operator.IN: { - const values = value?.arrayValue?.values?.map((val: any) => - Constant._fromProto(val) - ); - if (!values) { - return and(fieldValue.exists(), fieldValue.equalAny([])); - } else if (values.length === 1) { - return and(fieldValue.exists(), fieldValue.equal(values[0])); - } else { - return and(fieldValue.exists(), fieldValue.equalAny(values)); - } - } - case Operator.ARRAY_CONTAINS_ANY: { - const values = value?.arrayValue?.values?.map((val: any) => - Constant._fromProto(val) - ); - return and(fieldValue.exists(), fieldValue.arrayContainsAny(values!)); - } - case Operator.NOT_IN: { - const values = value?.arrayValue?.values?.map((val: any) => - Constant._fromProto(val) - ); - if (!values) { - return and(fieldValue.exists(), fieldValue.notEqualAny([])); - } else if (values.length === 1) { - return and(fieldValue.exists(), fieldValue.notEqual(values[0])); - } else { - return and(fieldValue.exists(), fieldValue.notEqualAny(values)); - } + case Operator.NOT_IN: { + const values = value?.arrayValue?.values?.map((val: any) => + Constant._fromProto(val) + ); + if (!values) { + return and(fieldValue.exists(), fieldValue.notEqualAny([])); + } else if (values.length === 1) { + return and(fieldValue.exists(), fieldValue.notEqual(values[0])); + } else { + return and(fieldValue.exists(), fieldValue.notEqualAny(values)); } - default: - fail(0x9047, 'Unexpected operator'); } + default: + fail(0x9047, 'Unexpected operator'); } } else if (f instanceof CompositeFilterInternal) { switch (f.op) { diff --git a/packages/firestore/src/global.ts b/packages/firestore/src/global.ts new file mode 100644 index 00000000000..529f43e022b --- /dev/null +++ b/packages/firestore/src/global.ts @@ -0,0 +1,23 @@ +/** + * @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. + */ + +// This file supports a special internal build that includes the entire +// Firestore classic and pipeline api surface in one bundle. + +import * as pipelines from './api_pipelines'; +export * from './api'; +export { pipelines }; diff --git a/packages/firestore/src/lite-api/aggregate_types.ts b/packages/firestore/src/lite-api/aggregate_types.ts index a87b3ca695d..292a2edd4f7 100644 --- a/packages/firestore/src/lite-api/aggregate_types.ts +++ b/packages/firestore/src/lite-api/aggregate_types.ts @@ -16,8 +16,13 @@ */ import { AggregateType } from '../core/aggregate'; +import { ObjectValue } from '../model/object_value'; import { FieldPath as InternalFieldPath } from '../model/path'; -import { ApiClientObjectMap, Value } from '../protos/firestore_proto_api'; +import { + ApiClientObjectMap, + firestoreV1ApiClientInterfaces, + Value +} from '../protos/firestore_proto_api'; import { average, count, sum } from './aggregate'; import { DocumentData, Query } from './reference'; @@ -116,4 +121,22 @@ export class AggregateQuerySnapshot< this._data ) as AggregateSpecData; } + + /** + * @internal + * @private + * + * Retrieves all fields in the snapshot as a proto value. + * + * @returns An `Object` containing all fields in the snapshot. + */ + _fieldsProto(): { [key: string]: firestoreV1ApiClientInterfaces.Value } { + // Wrap data in an ObjectValue to clone it. + const dataClone = new ObjectValue({ + mapValue: { fields: this._data } + }).clone(); + + // Return the cloned value to prevent manipulation of the Snapshot's data + return dataClone.value.mapValue.fields!; + } } diff --git a/packages/firestore/src/lite-api/components.ts b/packages/firestore/src/lite-api/components.ts index 52c3b3729ee..d956fcd31f5 100644 --- a/packages/firestore/src/lite-api/components.ts +++ b/packages/firestore/src/lite-api/components.ts @@ -75,6 +75,7 @@ export function getDatastore(firestore: FirestoreService): Datastore { firestore._databaseId, firestore.app.options.appId || '', firestore._persistenceKey, + firestore.app.options.apiKey, firestore._freezeSettings() ); const connection = newConnection(databaseInfo); @@ -108,6 +109,7 @@ export function makeDatabaseInfo( databaseId: DatabaseId, appId: string, persistenceKey: string, + apiKey: string | undefined, settings: FirestoreSettingsImpl ): DatabaseInfo { return new DatabaseInfo( @@ -120,6 +122,7 @@ export function makeDatabaseInfo( settings.experimentalAutoDetectLongPolling, cloneLongPollingOptions(settings.experimentalLongPollingOptions), settings.useFetchStreams, - settings.isUsingEmulator + settings.isUsingEmulator, + apiKey ); } diff --git a/packages/firestore/src/lite-api/expressions.ts b/packages/firestore/src/lite-api/expressions.ts index c30b2444cf9..d2efda000bb 100644 --- a/packages/firestore/src/lite-api/expressions.ts +++ b/packages/firestore/src/lite-api/expressions.ts @@ -171,6 +171,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that subtracts another expression from this expression. * * ```typescript @@ -184,6 +185,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { subtract(subtrahend: Expression): FunctionExpression; /** + * @beta * Creates an expression that subtracts a constant value from this expression. * * ```typescript @@ -204,6 +206,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that multiplies this expression by another expression. * * ```typescript @@ -224,6 +227,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that divides this expression by another expression. * * ```typescript @@ -237,6 +241,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { divide(divisor: Expression): FunctionExpression; /** + * @beta * Creates an expression that divides this expression by a constant value. * * ```typescript @@ -257,6 +262,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that calculates the modulo (remainder) of dividing this expression by another expression. * * ```typescript @@ -270,6 +276,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { mod(expression: Expression): FunctionExpression; /** + * @beta * Creates an expression that calculates the modulo (remainder) of dividing this expression by a constant value. * * ```typescript @@ -290,6 +297,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if this expression is equal to another expression. * * ```typescript @@ -303,6 +311,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { equal(expression: Expression): BooleanExpression; /** + * @beta * Creates an expression that checks if this expression is equal to a constant value. * * ```typescript @@ -323,6 +332,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if this expression is not equal to another expression. * * ```typescript @@ -336,6 +346,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { notEqual(expression: Expression): BooleanExpression; /** + * @beta * Creates an expression that checks if this expression is not equal to a constant value. * * ```typescript @@ -356,6 +367,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if this expression is less than another expression. * * ```typescript @@ -369,6 +381,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { lessThan(experession: Expression): BooleanExpression; /** + * @beta * Creates an expression that checks if this expression is less than a constant value. * * ```typescript @@ -389,6 +402,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if this expression is less than or equal to another * expression. * @@ -403,6 +417,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { lessThanOrEqual(expression: Expression): BooleanExpression; /** + * @beta * Creates an expression that checks if this expression is less than or equal to a constant value. * * ```typescript @@ -423,6 +438,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if this expression is greater than another expression. * * ```typescript @@ -436,6 +452,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { greaterThan(expression: Expression): BooleanExpression; /** + * @beta * Creates an expression that checks if this expression is greater than a constant value. * * ```typescript @@ -456,6 +473,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if this expression is greater than or equal to another * expression. * @@ -470,6 +488,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { greaterThanOrEqual(expression: Expression): BooleanExpression; /** + * @beta * Creates an expression that checks if this expression is greater than or equal to a constant * value. * @@ -491,6 +510,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that concatenates an array expression with one or more other arrays. * * ```typescript @@ -515,6 +535,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if an array contains a specific element. * * ```typescript @@ -528,6 +549,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { arrayContains(expression: Expression): BooleanExpression; /** + * @beta * Creates an expression that checks if an array contains a specific value. * * ```typescript @@ -548,6 +570,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if an array contains all the specified elements. * * ```typescript @@ -561,6 +584,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { arrayContainsAll(values: Array): BooleanExpression; /** + * @beta * Creates an expression that checks if an array contains all the specified elements. * * ```typescript @@ -584,6 +608,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if an array contains any of the specified elements. * * ```typescript @@ -597,6 +622,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { arrayContainsAny(values: Array): BooleanExpression; /** + * @beta * Creates an expression that checks if an array contains any of the specified elements. * * ```typescript @@ -623,6 +649,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that reverses an array. * * ```typescript @@ -637,6 +664,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that calculates the length of an array. * * ```typescript @@ -651,6 +679,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if this expression is equal to any of the provided values or * expressions. * @@ -665,6 +694,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { equalAny(values: Array): BooleanExpression; /** + * @beta * Creates an expression that checks if this expression is equal to any of the provided values or * expressions. * @@ -685,6 +715,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if this expression is not equal to any of the provided values or * expressions. * @@ -699,6 +730,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { notEqualAny(values: Array): BooleanExpression; /** + * @beta * Creates an expression that checks if this expression is not equal to any of the values in the evaluated expression. * * ```typescript @@ -722,34 +754,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** - * Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NaN - * field("value").divide(0).isNaN(); - * ``` - * - * @return A new `Expr` representing the 'isNaN' check. - */ - isNan(): BooleanExpression { - return new BooleanExpression('is_nan', [this], 'isNan'); - } - - /** - * Creates an expression that checks if this expression evaluates to 'Null'. - * - * ```typescript - * // Check if the result of a calculation is NaN - * field("value").isNull(); - * ``` - * - * @return A new `Expr` representing the 'isNull' check. - */ - isNull(): BooleanExpression { - return new BooleanExpression('is_null', [this], 'isNull'); - } - - /** + * @beta * Creates an expression that checks if a field exists in the document. * * ```typescript @@ -764,6 +769,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that calculates the character length of a string in UTF-8. * * ```typescript @@ -778,6 +784,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that performs a case-sensitive string comparison. * * ```typescript @@ -791,6 +798,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { like(pattern: string): BooleanExpression; /** + * @beta * Creates an expression that performs a case-sensitive string comparison. * * ```typescript @@ -811,6 +819,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if a string contains a specified regular expression as a * substring. * @@ -825,6 +834,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { regexContains(pattern: string): BooleanExpression; /** + * @beta * Creates an expression that checks if a string contains a specified regular expression as a * substring. * @@ -846,6 +856,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if a string matches a specified regular expression. * * ```typescript @@ -859,6 +870,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { regexMatch(pattern: string): BooleanExpression; /** + * @beta * Creates an expression that checks if a string matches a specified regular expression. * * ```typescript @@ -879,6 +891,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if a string contains a specified substring. * * ```typescript @@ -892,6 +905,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { stringContains(substring: string): BooleanExpression; /** + * @beta * Creates an expression that checks if a string contains the string represented by another expression. * * ```typescript @@ -912,6 +926,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if a string starts with a given prefix. * * ```typescript @@ -925,6 +940,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { startsWith(prefix: string): BooleanExpression; /** + * @beta * Creates an expression that checks if a string starts with a given prefix (represented as an * expression). * @@ -946,6 +962,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that checks if a string ends with a given postfix. * * ```typescript @@ -959,6 +976,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { endsWith(suffix: string): BooleanExpression; /** + * @beta * Creates an expression that checks if a string ends with a given postfix (represented as an * expression). * @@ -980,6 +998,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that converts a string to lowercase. * * ```typescript @@ -994,6 +1013,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that converts a string to uppercase. * * ```typescript @@ -1008,20 +1028,30 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** - * Creates an expression that removes leading and trailing whitespace from a string. + * @beta + * Creates an expression that removes leading and trailing characters from a string or byte array. * * ```typescript * // Trim whitespace from the 'userInput' field * field("userInput").trim(); - * ``` * - * @return A new `Expr` representing the trimmed string. - */ - trim(): FunctionExpression { - return new FunctionExpression('trim', [this], 'trim'); + * // Trim quotes from the 'userInput' field + * field("userInput").trim('"'); + * ``` + * @param valueToTrim Optional This parameter is treated as a set of characters or bytes that will be + * trimmed from the input. If not specified, then whitespace will be trimmed. + * @return A new `Expr` representing the trimmed string or byte array. + */ + trim(valueToTrim?: string | Expression | Bytes): FunctionExpression { + const args: Expression[] = [this]; + if (valueToTrim) { + args.push(valueToDefaultExpr(valueToTrim)); + } + return new FunctionExpression('trim', args, 'trim'); } /** + * @beta * Creates an expression that concatenates string expressions together. * * ```typescript @@ -1047,6 +1077,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that concatenates expression results together. * * ```typescript @@ -1068,6 +1099,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that reverses this string expression. * * ```typescript @@ -1082,6 +1114,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that calculates the length of this string expression in bytes. * * ```typescript @@ -1096,6 +1129,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that computes the ceiling of a numeric value. * * ```typescript @@ -1110,6 +1144,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that computes the floor of a numeric value. * * ```typescript @@ -1124,6 +1159,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that computes the absolute value of a numeric value. * * ```typescript @@ -1138,6 +1174,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that computes e to the power of this expression. * * ```typescript @@ -1152,6 +1189,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Accesses a value from a map (object) field using the provided key. * * ```typescript @@ -1171,6 +1209,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an aggregation that counts the number of stage inputs with valid evaluations of the * expression or field. * @@ -1182,10 +1221,11 @@ export abstract class Expression implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'count' aggregation. */ count(): AggregateFunction { - return new AggregateFunction('count', [this], 'count'); + return AggregateFunction._create('count', [this], 'count'); } /** + * @beta * Creates an aggregation that calculates the sum of a numeric field across multiple stage inputs. * * ```typescript @@ -1196,10 +1236,11 @@ export abstract class Expression implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'sum' aggregation. */ sum(): AggregateFunction { - return new AggregateFunction('sum', [this], 'sum'); + return AggregateFunction._create('sum', [this], 'sum'); } /** + * @beta * Creates an aggregation that calculates the average (mean) of a numeric field across multiple * stage inputs. * @@ -1211,10 +1252,11 @@ export abstract class Expression implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'average' aggregation. */ average(): AggregateFunction { - return new AggregateFunction('average', [this], 'average'); + return AggregateFunction._create('average', [this], 'average'); } /** + * @beta * Creates an aggregation that finds the minimum value of a field across multiple stage inputs. * * ```typescript @@ -1225,10 +1267,11 @@ export abstract class Expression implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'minimum' aggregation. */ minimum(): AggregateFunction { - return new AggregateFunction('minimum', [this], 'minimum'); + return AggregateFunction._create('minimum', [this], 'minimum'); } /** + * @beta * Creates an aggregation that finds the maximum value of a field across multiple stage inputs. * * ```typescript @@ -1239,10 +1282,11 @@ export abstract class Expression implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'maximum' aggregation. */ maximum(): AggregateFunction { - return new AggregateFunction('maximum', [this], 'maximum'); + return AggregateFunction._create('maximum', [this], 'maximum'); } /** + * @beta * Creates an aggregation that counts the number of distinct values of the expression or field. * * ```typescript @@ -1253,10 +1297,11 @@ export abstract class Expression implements ProtoValueSerializable, UserData { * @return A new `AggregateFunction` representing the 'count_distinct' aggregation. */ countDistinct(): AggregateFunction { - return new AggregateFunction('count_distinct', [this], 'countDistinct'); + return AggregateFunction._create('count_distinct', [this], 'countDistinct'); } /** + * @beta * Creates an expression that returns the larger value between this expression and another expression, based on Firestore's value type ordering. * * ```typescript @@ -1281,6 +1326,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that returns the smaller value between this expression and another expression, based on Firestore's value type ordering. * * ```typescript @@ -1305,6 +1351,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that calculates the length (number of dimensions) of this Firestore Vector expression. * * ```typescript @@ -1319,6 +1366,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Calculates the cosine distance between two vectors. * * ```typescript @@ -1331,6 +1379,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { */ cosineDistance(vectorExpression: Expression): FunctionExpression; /** + * @beta * Calculates the Cosine distance between two vectors. * * ```typescript @@ -1353,6 +1402,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Calculates the dot product between two vectors. * * ```typescript @@ -1366,6 +1416,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { dotProduct(vectorExpression: Expression): FunctionExpression; /** + * @beta * Calculates the dot product between two vectors. * * ```typescript @@ -1386,6 +1437,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Calculates the Euclidean distance between two vectors. * * ```typescript @@ -1399,6 +1451,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { euclideanDistance(vectorExpression: Expression): FunctionExpression; /** + * @beta * Calculates the Euclidean distance between two vectors. * * ```typescript @@ -1421,6 +1474,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that interprets this expression as the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC) * and returns a timestamp. * @@ -1440,6 +1494,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that converts this timestamp expression to the number of microseconds since the Unix epoch (1970-01-01 00:00:00 UTC). * * ```typescript @@ -1458,6 +1513,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that interprets this expression as the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC) * and returns a timestamp. * @@ -1477,6 +1533,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that converts this timestamp expression to the number of milliseconds since the Unix epoch (1970-01-01 00:00:00 UTC). * * ```typescript @@ -1495,6 +1552,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that interprets this expression as the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC) * and returns a timestamp. * @@ -1514,6 +1572,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that converts this timestamp expression to the number of seconds since the Unix epoch (1970-01-01 00:00:00 UTC). * * ```typescript @@ -1532,6 +1591,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that adds a specified amount of time to this timestamp expression. * * ```typescript @@ -1546,6 +1606,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { timestampAdd(unit: Expression, amount: Expression): FunctionExpression; /** + * @beta * Creates an expression that adds a specified amount of time to this timestamp expression. * * ```typescript @@ -1580,6 +1641,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that subtracts a specified amount of time from this timestamp expression. * * ```typescript @@ -1594,6 +1656,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { timestampSubtract(unit: Expression, amount: Expression): FunctionExpression; /** + * @beta * Creates an expression that subtracts a specified amount of time from this timestamp expression. * * ```typescript @@ -1800,38 +1863,6 @@ export abstract class Expression implements ProtoValueSerializable, UserData { return new BooleanExpression('is_absent', [this], 'isAbsent'); } - /** - * @beta - * - * Creates an expression that checks if tbe result of an expression is not null. - * - * ```typescript - * // Check if the value of the 'name' field is not null - * field("name").isNotNull(); - * ``` - * - * @return A new {@code BooleanExpr} representing the 'isNotNull' check. - */ - isNotNull(): BooleanExpression { - return new BooleanExpression('is_not_null', [this], 'isNotNull'); - } - - /** - * @beta - * - * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NOT NaN - * field("value").divide(0).isNotNan(); - * ``` - * - * @return A new {@code Expr} representing the 'isNaN' check. - */ - isNotNan(): BooleanExpression { - return new BooleanExpression('is_not_nan', [this], 'isNotNan'); - } - /** * @beta * @@ -1900,6 +1931,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that returns the value of this expression raised to the power of another expression. * * ```typescript @@ -1913,6 +1945,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { pow(exponent: Expression): FunctionExpression; /** + * @beta * Creates an expression that returns the value of this expression raised to the power of a constant value. * * ```typescript @@ -1929,6 +1962,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that rounds a numeric value to the nearest whole number. * * ```typescript @@ -1940,6 +1974,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { */ round(): FunctionExpression; /** + * @beta * Creates an expression that rounds a numeric value to the specified number of decimal places. * * ```typescript @@ -1953,6 +1988,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { */ round(decimalPlaces: number): FunctionExpression; /** + * @beta * Creates an expression that rounds a numeric value to the specified number of decimal places. * * ```typescript @@ -1978,6 +2014,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that returns the collection ID from a path. * * ```typescript @@ -1992,6 +2029,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that calculates the length of a string, array, map, vector, or bytes. * * ```typescript @@ -2009,6 +2047,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that computes the natural logarithm of a numeric value. * * ```typescript @@ -2023,6 +2062,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that computes the square root of a numeric value. * * ```typescript @@ -2037,6 +2077,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that reverses a string. * * ```typescript @@ -2051,6 +2092,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * 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. * @@ -2066,6 +2108,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { ifAbsent(elseValue: unknown): Expression; /** + * @beta * Creates an expression that returns the `elseValue` argument if this expression results in an absent value, else * return the result of this expression evaluation. * @@ -2089,6 +2132,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that joins the elements of an array into a string. * * ```typescript @@ -2102,6 +2146,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { join(delimiterExpression: Expression): Expression; /** + * @beta * Creates an expression that joins the elements of an array field into a string. * * ```typescript @@ -2123,6 +2168,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that computes the base-10 logarithm of a numeric value. * * ```typescript @@ -2137,6 +2183,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an expression that computes the sum of the elements in an array. * * ```typescript @@ -2150,9 +2197,114 @@ export abstract class Expression implements ProtoValueSerializable, UserData { return new FunctionExpression('sum', [this]); } + /** + * @beta + * Creates an expression that splits the result of this expression into an + * array of substrings based on the provided delimiter. + * + * @example + * ```typescript + * // Split the 'scoresCsv' field on delimiter ',' + * field('scoresCsv').split(',') + * ``` + * + * @return A new {@code Expression} representing the split function. + */ + split(delimiter: string): FunctionExpression; + + /** + * @beta + * Creates an expression that splits the result of this expression into an + * array of substrings based on the provided delimiter. + * + * @example + * ```typescript + * // Split the 'scores' field on delimiter ',' or ':' depending on the stored format + * field('scores').split(conditional(field('format').equal('csv'), constant(','), constant(':')) + * ``` + * + * @return A new {@code Expression} representing the split function. + */ + split(delimiter: Expression): FunctionExpression; + split(delimiter: string | Expression): FunctionExpression { + return new FunctionExpression('split', [ + this, + valueToDefaultExpr(delimiter) + ]); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @example + * ```typescript + * // Truncate the 'createdAt' timestamp to the beginning of the day. + * field('createdAt').timestampTruncate('day') + * ``` + * + * @param granularity The granularity to truncate to. + * @param timezone The timezone to use for truncation. Valid values are from + * the TZ database (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {Expression} representing the truncated timestamp. + */ + timestampTruncate( + granularity: TimeGranularity, + timezone?: string | Expression + ): FunctionExpression; + + /** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @example + * ```typescript + * // Truncate the 'createdAt' timestamp to the granularity specified in the field 'granularity'. + * field('createdAt').timestampTruncate(field('granularity')) + * ``` + * + * @param granularity The granularity to truncate to. + * @param timezone The timezone to use for truncation. Valid values are from + * the TZ database (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {Expression} representing the truncated timestamp. + */ + timestampTruncate( + granularity: Expression, + timezone?: string | Expression + ): FunctionExpression; + timestampTruncate( + granularity: TimeGranularity | Expression, + timezone?: string | Expression + ): FunctionExpression { + const internalGranularity = isString(granularity) + ? granularity.toLowerCase() + : granularity; + + const args = [this, valueToDefaultExpr(internalGranularity)]; + if (timezone) { + args.push(valueToDefaultExpr(timezone)); + } + return new FunctionExpression('timestamp_trunc', args); + } + + /** + * @beta + * Creates an expression that returns the data type of this expression's result, as a string. + * + * @example + * ```typescript + * // Get the data type of the value in field 'title' + * field('title').type() + * ``` + * + * @return A new {Expression} representing the data type. + */ + type(): FunctionExpression { + return new FunctionExpression('type', [this]); + } + // TODO(new-expression): Add new expression method definitions above this line /** + * @beta * Creates an {@link Ordering} that sorts documents in ascending order based on this expression. * * ```typescript @@ -2168,6 +2320,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Creates an {@link Ordering} that sorts documents in descending order based on this expression. * * ```typescript @@ -2183,6 +2336,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } /** + * @beta * Assigns an alias to this expression. * * Aliases are useful for renaming fields in the output of a stage or for giving meaningful @@ -2203,6 +2357,27 @@ export abstract class Expression implements ProtoValueSerializable, UserData { } } +export type TimeGranularity = + | 'microsecond' + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day' + | 'week' + | 'week(monday)' + | 'week(tuesday)' + | 'week(wednesday)' + | 'week(thursday)' + | 'week(friday)' + | 'week(saturday)' + | 'week(sunday)' + | 'isoWeek' + | 'month' + | 'quarter' + | 'year' + | 'isoYear'; + /** * @beta * @@ -2230,26 +2405,30 @@ export interface Selectable { export class AggregateFunction implements ProtoValueSerializable, UserData { exprType: ExpressionType = 'AggregateFunction'; - constructor(name: string, params: Expression[]); /** - * INTERNAL Constructor with method name for validation. - * @hideconstructor - * @param name - * @param params - * @param _methodName + * @internal */ - constructor( + _methodName?: string; + + constructor(private name: string, private params: Expression[]) {} + + /** + * @internal + * @private + */ + static _create( name: string, params: Expression[], - _methodName: string | undefined - ); - constructor( - private name: string, - private params: Expression[], - readonly _methodName?: string - ) {} + methodName: string + ): AggregateFunction { + const af = new AggregateFunction(name, params); + af._methodName = methodName; + + return af; + } /** + * @beta * Assigns an alias to this AggregateFunction. The alias specifies the name that * the aggregated value will have in the output document. * @@ -2438,6 +2617,7 @@ export class Field extends Expression implements Selectable { } /** + * @beta * Creates a {@code Field} instance representing the field at the given path. * * The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field @@ -2547,6 +2727,7 @@ export class Constant extends Expression { } /** + * @beta * Creates a `Constant` instance for a number value. * * @param value The number value. @@ -2555,6 +2736,7 @@ export class Constant extends Expression { export function constant(value: number): Expression; /** + * @beta * Creates a `Constant` instance for a string value. * * @param value The string value. @@ -2563,6 +2745,7 @@ export function constant(value: number): Expression; export function constant(value: string): Expression; /** + * @beta * Creates a `BooleanExpression` instance for a boolean value. * * @param value The boolean value. @@ -2571,6 +2754,7 @@ export function constant(value: string): Expression; export function constant(value: boolean): BooleanExpression; /** + * @beta * Creates a `Constant` instance for a null value. * * @param value The null value. @@ -2579,6 +2763,7 @@ export function constant(value: boolean): BooleanExpression; export function constant(value: null): Expression; /** + * @beta * Creates a `Constant` instance for a GeoPoint value. * * @param value The GeoPoint value. @@ -2587,6 +2772,7 @@ export function constant(value: null): Expression; export function constant(value: GeoPoint): Expression; /** + * @beta * Creates a `Constant` instance for a Timestamp value. * * @param value The Timestamp value. @@ -2595,6 +2781,7 @@ export function constant(value: GeoPoint): Expression; export function constant(value: Timestamp): Expression; /** + * @beta * Creates a `Constant` instance for a Date value. * * @param value The Date value. @@ -2603,6 +2790,7 @@ export function constant(value: Timestamp): Expression; export function constant(value: Date): Expression; /** + * @beta * Creates a `Constant` instance for a Bytes value. * * @param value The Bytes value. @@ -2611,6 +2799,7 @@ export function constant(value: Date): Expression; export function constant(value: Bytes): Expression; /** + * @beta * Creates a `Constant` instance for a DocumentReference value. * * @param value The DocumentReference value. @@ -2629,6 +2818,7 @@ export function constant(value: DocumentReference): Expression; export function constant(value: ProtoValue): Expression; /** + * @beta * Creates a `Constant` instance for a VectorValue value. * * @param value The VectorValue value. @@ -2748,6 +2938,7 @@ export class BooleanExpression extends FunctionExpression { filterable: true = true; /** + * @beta * Creates an aggregation that finds the count of input documents satisfying * this boolean expression. * @@ -2759,10 +2950,11 @@ export class BooleanExpression extends FunctionExpression { * @return A new `AggregateFunction` representing the 'countIf' aggregation. */ countIf(): AggregateFunction { - return new AggregateFunction('count_if', [this], 'countIf'); + return AggregateFunction._create('count_if', [this], 'countIf'); } /** + * @beta * Creates an expression that negates this boolean expression. * * ```typescript @@ -2777,6 +2969,7 @@ export class BooleanExpression extends FunctionExpression { } /** + * @beta * 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`. @@ -3091,105 +3284,6 @@ export function isAbsent(value: Expression | string): BooleanExpression { return fieldOrExpression(value).isAbsent(); } -/** - * @beta - * - * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NaN - * isNaN(field("value").divide(0)); - * ``` - * - * @param value The expression to check. - * @return A new {@code Expr} representing the 'isNaN' check. - */ -export function isNull(value: Expression): BooleanExpression; - -/** - * @beta - * - * Creates an expression that checks if a field's value evaluates to 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NaN - * isNaN("value"); - * ``` - * - * @param value The name of the field to check. - * @return A new {@code Expr} representing the 'isNaN' check. - */ -export function isNull(value: string): BooleanExpression; -export function isNull(value: Expression | string): BooleanExpression { - return fieldOrExpression(value).isNull(); -} - -/** - * @beta - * - * Creates an expression that checks if tbe result of an expression is not null. - * - * ```typescript - * // Check if the value of the 'name' field is not null - * isNotNull(field("name")); - * ``` - * - * @param value The expression to check. - * @return A new {@code Expr} representing the 'isNaN' check. - */ -export function isNotNull(value: Expression): BooleanExpression; - -/** - * @beta - * - * Creates an expression that checks if tbe value of a field is not null. - * - * ```typescript - * // Check if the value of the 'name' field is not null - * isNotNull("name"); - * ``` - * - * @param value The name of the field to check. - * @return A new {@code Expr} representing the 'isNaN' check. - */ -export function isNotNull(value: string): BooleanExpression; -export function isNotNull(value: Expression | string): BooleanExpression { - return fieldOrExpression(value).isNotNull(); -} - -/** - * @beta - * - * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NOT NaN - * isNotNaN(field("value").divide(0)); - * ``` - * - * @param value The expression to check. - * @return A new {@code Expr} representing the 'isNotNaN' check. - */ -export function isNotNan(value: Expression): BooleanExpression; - -/** - * @beta - * - * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a Number). - * - * ```typescript - * // Check if the value of a field is NOT NaN - * isNotNaN("value"); - * ``` - * - * @param value The name of the field to check. - * @return A new {@code Expr} representing the 'isNotNaN' check. - */ -export function isNotNan(value: string): BooleanExpression; -export function isNotNan(value: Expression | string): BooleanExpression { - return fieldOrExpression(value).isNotNan(); -} - /** * @beta * @@ -4114,6 +4208,7 @@ export function lessThanOrEqual( ): BooleanExpression; /** + * @beta * Creates an expression that checks if a field's value is less than or equal to an expression. * * ```typescript @@ -5072,39 +5167,6 @@ export function exists(valueOrField: Expression | string): BooleanExpression { return fieldOrExpression(valueOrField).exists(); } -/** - * @beta - * - * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NaN - * isNaN(field("value").divide(0)); - * ``` - * - * @param value The expression to check. - * @return A new {@code Expr} representing the 'isNaN' check. - */ -export function isNan(value: Expression): BooleanExpression; - -/** - * @beta - * - * Creates an expression that checks if a field's value evaluates to 'NaN' (Not a Number). - * - * ```typescript - * // Check if the result of a calculation is NaN - * isNaN("value"); - * ``` - * - * @param fieldName The name of the field to check. - * @return A new {@code Expr} representing the 'isNaN' check. - */ -export function isNan(fieldName: string): BooleanExpression; -export function isNan(value: Expression | string): BooleanExpression { - return fieldOrExpression(value).isNan(); -} - /** * @beta * @@ -5173,6 +5235,7 @@ export function byteLength(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that reverses an array. * * ```typescript @@ -5186,6 +5249,7 @@ export function byteLength(expr: Expression | string): FunctionExpression { export function arrayReverse(fieldName: string): FunctionExpression; /** + * @beta * Creates an expression that reverses an array. * * ```typescript @@ -5202,6 +5266,7 @@ export function arrayReverse(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that computes e to the power of the expression's result. * * ```typescript @@ -5214,6 +5279,7 @@ export function arrayReverse(expr: Expression | string): FunctionExpression { export function exp(expression: Expression): FunctionExpression; /** + * @beta * Creates an expression that computes e to the power of the expression's result. * * ```typescript @@ -5232,6 +5298,7 @@ export function exp( } /** + * @beta * Creates an expression that computes the ceiling of a numeric value. * * ```typescript @@ -5245,6 +5312,7 @@ export function exp( export function ceil(fieldName: string): FunctionExpression; /** + * @beta * Creates an expression that computes the ceiling of a numeric value. * * ```typescript @@ -5261,6 +5329,7 @@ export function ceil(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that computes the floor of a numeric value. * * @param expr The expression to compute the floor of. @@ -5269,6 +5338,7 @@ export function ceil(expr: Expression | string): FunctionExpression { export function floor(expr: Expression): FunctionExpression; /** + * @beta * Creates an expression that computes the floor of a numeric value. * * @param fieldName The name of the field to compute the floor of. @@ -5280,6 +5350,7 @@ export function floor(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an aggregation that counts the number of distinct values of a field. * * @param expr The expression or field to count distinct values of. @@ -5891,34 +5962,53 @@ export function toUpper(expr: Expression | string): FunctionExpression { /** * @beta * - * Creates an expression that removes leading and trailing whitespace from a string field. + * Creates an expression that removes leading and trailing whitespace from a string or byte array. * * ```typescript * // Trim whitespace from the 'userInput' field * trim("userInput"); + * + * // Trim quotes from the 'userInput' field + * trim("userInput", '"'); * ``` * - * @param fieldName The name of the field containing the string. + * @param fieldName The name of the field containing the string or byte array. + * @param valueToTrim Optional This parameter is treated as a set of characters or bytes that will be + * trimmed from the input. If not specified, then whitespace will be trimmed. * @return A new {@code Expr} representing the trimmed string. */ -export function trim(fieldName: string): FunctionExpression; +export function trim( + fieldName: string, + valueToTrim?: string | Expression +): FunctionExpression; /** * @beta * - * Creates an expression that removes leading and trailing whitespace from a string expression. + * Creates an expression that removes leading and trailing characters from a string or byte array expression. * * ```typescript * // Trim whitespace from the 'userInput' field * trim(field("userInput")); + * + * // Trim quotes from the 'userInput' field + * trim(field("userInput"), '"'); * ``` * - * @param stringExpression The expression representing the string to trim. - * @return A new {@code Expr} representing the trimmed string. + * @param stringExpression The expression representing the string or byte array to trim. + * @param valueToTrim Optional This parameter is treated as a set of characters or bytes that will be + * trimmed from the input. If not specified, then whitespace will be trimmed. + * @return A new {@code Expr} representing the trimmed string or byte array. */ -export function trim(stringExpression: Expression): FunctionExpression; -export function trim(expr: Expression | string): FunctionExpression { - return fieldOrExpression(expr).trim(); +export function trim( + stringExpression: Expression, + valueToTrim?: string | Expression +): FunctionExpression; +export function trim( + expr: Expression | string, + valueToTrim?: string | Expression +): FunctionExpression { + return fieldOrExpression(expr).trim(valueToTrim); } /** @@ -6026,7 +6116,7 @@ export function mapGet( * @return A new {@code AggregateFunction} representing the 'countAll' aggregation. */ export function countAll(): AggregateFunction { - return new AggregateFunction('count', [], 'count'); + return AggregateFunction._create('count', [], 'count'); } /** @@ -6046,6 +6136,7 @@ export function countAll(): AggregateFunction { export function count(expression: Expression): AggregateFunction; /** + * @beta * Creates an aggregation that counts the number of stage inputs where the input field exists. * * ```typescript @@ -6884,25 +6975,6 @@ 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 * @@ -6952,6 +7024,7 @@ export function or( } /** + * @beta * Creates an expression that returns the value of the base expression raised to the power of the exponent expression. * * ```typescript @@ -6966,6 +7039,7 @@ export function or( export function pow(base: Expression, exponent: Expression): FunctionExpression; /** + * @beta * Creates an expression that returns the value of the base expression raised to the power of the exponent. * * ```typescript @@ -6980,6 +7054,7 @@ export function pow(base: Expression, exponent: Expression): FunctionExpression; export function pow(base: Expression, exponent: number): FunctionExpression; /** + * @beta * Creates an expression that returns the value of the base field raised to the power of the exponent expression. * * ```typescript @@ -6994,6 +7069,7 @@ export function pow(base: Expression, exponent: number): FunctionExpression; export function pow(base: string, exponent: Expression): FunctionExpression; /** + * @beta * Creates an expression that returns the value of the base field raised to the power of the exponent. * * ```typescript @@ -7014,6 +7090,7 @@ export function pow( } /** + * @beta * Creates an expression that rounds a numeric value to the nearest whole number. * * ```typescript @@ -7027,6 +7104,7 @@ export function pow( export function round(fieldName: string): FunctionExpression; /** + * @beta * Creates an expression that rounds a numeric value to the nearest whole number. * * ```typescript @@ -7040,6 +7118,7 @@ export function round(fieldName: string): FunctionExpression; export function round(expression: Expression): FunctionExpression; /** + * @beta * Creates an expression that rounds a numeric value to the specified number of decimal places. * * ```typescript @@ -7057,6 +7136,7 @@ export function round( ): FunctionExpression; /** + * @beta * Creates an expression that rounds a numeric value to the specified number of decimal places. * * ```typescript @@ -7084,6 +7164,7 @@ export function round( } /** + * @beta * Creates an expression that returns the collection ID from a path. * * ```typescript @@ -7097,6 +7178,7 @@ export function round( export function collectionId(fieldName: string): FunctionExpression; /** + * @beta * Creates an expression that returns the collection ID from a path. * * ```typescript @@ -7113,6 +7195,7 @@ export function collectionId(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that calculates the length of a string, array, map, vector, or bytes. * * ```typescript @@ -7129,6 +7212,7 @@ export function collectionId(expr: Expression | string): FunctionExpression { export function length(fieldName: string): FunctionExpression; /** + * @beta * Creates an expression that calculates the length of a string, array, map, vector, or bytes. * * ```typescript @@ -7148,6 +7232,7 @@ export function length(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that computes the natural logarithm of a numeric value. * * ```typescript @@ -7161,6 +7246,7 @@ export function length(expr: Expression | string): FunctionExpression { export function ln(fieldName: string): FunctionExpression; /** + * @beta * Creates an expression that computes the natural logarithm of a numeric value. * * ```typescript @@ -7177,6 +7263,7 @@ export function ln(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that computes the logarithm of an expression to a given base. * * ```typescript @@ -7190,6 +7277,7 @@ export function ln(expr: Expression | string): FunctionExpression { */ export function log(expression: Expression, base: number): FunctionExpression; /** + * @beta * Creates an expression that computes the logarithm of an expression to a given base. * * ```typescript @@ -7206,6 +7294,7 @@ export function log( base: Expression ): FunctionExpression; /** + * @beta * Creates an expression that computes the logarithm of a field to a given base. * * ```typescript @@ -7219,6 +7308,7 @@ export function log( */ export function log(fieldName: string, base: number): FunctionExpression; /** + * @beta * Creates an expression that computes the logarithm of a field to a given base. * * ```typescript @@ -7242,6 +7332,7 @@ export function log( } /** + * @beta * Creates an expression that computes the square root of a numeric value. * * ```typescript @@ -7254,6 +7345,7 @@ export function log( */ export function sqrt(expression: Expression): FunctionExpression; /** + * @beta * Creates an expression that computes the square root of a numeric value. * * ```typescript @@ -7270,6 +7362,7 @@ export function sqrt(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that reverses a string. * * ```typescript @@ -7283,6 +7376,7 @@ export function sqrt(expr: Expression | string): FunctionExpression { export function stringReverse(stringExpression: Expression): FunctionExpression; /** + * @beta * Creates an expression that reverses a string value in the specified field. * * ```typescript @@ -7299,6 +7393,7 @@ export function stringReverse(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that concatenates strings, arrays, or blobs. Types cannot be mixed. * * ```typescript @@ -7318,6 +7413,7 @@ export function concat( ): FunctionExpression; /** + * @beta * Creates an expression that concatenates strings, arrays, or blobs. Types cannot be mixed. * * ```typescript @@ -7349,6 +7445,7 @@ export function concat( } /** + * @beta * Creates an expression that computes the absolute value of a numeric value. * * @param expr The expression to compute the absolute value of. @@ -7357,6 +7454,7 @@ export function concat( export function abs(expr: Expression): FunctionExpression; /** + * @beta * Creates an expression that computes the absolute value of a numeric value. * * @param fieldName The field to compute the absolute value of. @@ -7368,6 +7466,7 @@ export function abs(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that returns the `elseExpr` argument if `ifExpr` is absent, else return * the result of the `ifExpr` argument evaluation. * @@ -7384,6 +7483,7 @@ export function abs(expr: Expression | string): FunctionExpression { export function ifAbsent(ifExpr: Expression, elseExpr: Expression): Expression; /** + * @beta * Creates an expression that returns the `elseValue` argument if `ifExpr` is absent, else * return the result of the `ifExpr` argument evaluation. * @@ -7400,6 +7500,7 @@ export function ifAbsent(ifExpr: Expression, elseExpr: Expression): Expression; export function ifAbsent(ifExpr: Expression, elseValue: unknown): Expression; /** + * @beta * Creates an expression that returns the `elseExpr` argument if `ifFieldName` is absent, else * return the value of the field. * @@ -7417,6 +7518,7 @@ export function ifAbsent(ifExpr: Expression, elseValue: unknown): Expression; export function ifAbsent(ifFieldName: string, elseExpr: Expression): Expression; /** + * @beta * Creates an expression that returns the `elseValue` argument if `ifFieldName` is absent, else * return the value of the field. * @@ -7444,6 +7546,7 @@ export function ifAbsent( } /** + * @beta * Creates an expression that joins the elements of an array into a string. * * ```typescript @@ -7458,6 +7561,7 @@ export function ifAbsent( export function join(arrayFieldName: string, delimiter: string): Expression; /** + * @beta * Creates an expression that joins the elements of an array into a string. * * ```typescript @@ -7475,6 +7579,7 @@ export function join( ): Expression; /** + * @beta * Creates an expression that joins the elements of an array into a string. * * ```typescript @@ -7492,6 +7597,7 @@ export function join( ): Expression; /** + * @beta * Creates an expression that joins the elements of an array into a string. * * ```typescript @@ -7517,6 +7623,7 @@ export function join( } /** + * @beta * Creates an expression that computes the base-10 logarithm of a numeric value. * * ```typescript @@ -7530,6 +7637,7 @@ export function join( export function log10(fieldName: string): FunctionExpression; /** + * @beta * Creates an expression that computes the base-10 logarithm of a numeric value. * * ```typescript @@ -7546,6 +7654,7 @@ export function log10(expr: Expression | string): FunctionExpression { } /** + * @beta * Creates an expression that computes the sum of the elements in an array. * * ```typescript @@ -7559,6 +7668,7 @@ export function log10(expr: Expression | string): FunctionExpression { export function arraySum(fieldName: string): FunctionExpression; /** + * @beta * Creates an expression that computes the sum of the elements in an array. * * ```typescript @@ -7574,6 +7684,220 @@ export function arraySum(expr: Expression | string): FunctionExpression { return fieldOrExpression(expr).arraySum(); } +/** + * @beta + * Creates an expression that splits the value of a field on the provided delimiter. + * + * @example + * ```typescript + * // Split the 'scoresCsv' field on delimiter ',' + * split('scoresCsv', ',') + * ``` + * + * @param fieldName Split the value in this field. + * @param delimiter Split on this delimiter. + * + * @return A new {@code Expression} representing the split function. + */ +export function split(fieldName: string, delimiter: string): FunctionExpression; + +/** + * @beta + * Creates an expression that splits the value of a field on the provided delimiter. + * + * @example + * ```typescript + * // Split the 'scores' field on delimiter ',' or ':' depending on the stored format + * split('scores', conditional(field('format').equal('csv'), constant(','), constant(':')) + * ``` + * + * @param fieldName Split the value in this field. + * @param delimiter Split on this delimiter returned by evaluating this expression. + * + * @return A new {@code Expression} representing the split function. + */ +export function split( + fieldName: string, + delimiter: Expression +): FunctionExpression; + +/** + * @beta + * Creates an expression that splits a string into an array of substrings based on the provided delimiter. + * + * @example + * ```typescript + * // Split the 'scoresCsv' field on delimiter ',' + * split(field('scoresCsv'), ',') + * ``` + * + * @param expression Split the result of this expression. + * @param delimiter Split on this delimiter. + * + * @return A new {@code Expression} representing the split function. + */ +export function split( + expression: Expression, + delimiter: string +): FunctionExpression; + +/** + * @beta + * Creates an expression that splits a string into an array of substrings based on the provided delimiter. + * + * @example + * ```typescript + * // Split the 'scores' field on delimiter ',' or ':' depending on the stored format + * split(field('scores'), conditional(field('format').equal('csv'), constant(','), constant(':')) + * ``` + * + * @param expression Split the result of this expression. + * @param delimiter Split on this delimiter returned by evaluating this expression. + * + * @return A new {@code Expression} representing the split function. + */ +export function split( + expression: Expression, + delimiter: Expression +): FunctionExpression; +export function split( + fieldNameOrExpression: string | Expression, + delimiter: string | Expression +): FunctionExpression { + return fieldOrExpression(fieldNameOrExpression).split( + valueToDefaultExpr(delimiter) + ); +} + +/** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @example + * ```typescript + * // Truncate the 'createdAt' timestamp to the beginning of the day. + * field('createdAt').timestampTruncate('day') + * ``` + * + * @param fieldName Truncate the timestamp value contained in this field. + * @param granularity The granularity to truncate to. + * @param timezone The timezone to use for truncation. Valid values are from + * the TZ database (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {Expression} representing the truncated timestamp. + */ +export function timestampTruncate( + fieldName: string, + granularity: TimeGranularity, + timezone?: string | Expression +): FunctionExpression; + +/** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @example + * ```typescript + * // Truncate the 'createdAt' timestamp to the granularity specified in the field 'granularity'. + * field('createdAt').timestampTruncate(field('granularity')) + * ``` + * + * @param fieldName Truncate the timestamp value contained in this field. + * @param granularity The granularity to truncate to. + * @param timezone The timezone to use for truncation. Valid values are from + * the TZ database (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {Expression} representing the truncated timestamp. + */ +export function timestampTruncate( + fieldName: string, + granularity: Expression, + timezone?: string | Expression +): FunctionExpression; + +/** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @example + * ```typescript + * // Truncate the 'createdAt' timestamp to the beginning of the day. + * field('createdAt').timestampTruncate('day') + * ``` + * + * @param timestampExpression Truncate the timestamp value that is returned by this expression. + * @param granularity The granularity to truncate to. + * @param timezone The timezone to use for truncation. Valid values are from + * the TZ database (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {Expression} representing the truncated timestamp. + */ +export function timestampTruncate( + timestampExpression: Expression, + granularity: TimeGranularity, + timezone?: string | Expression +): FunctionExpression; + +/** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @example + * ```typescript + * // Truncate the 'createdAt' timestamp to the granularity specified in the field 'granularity'. + * field('createdAt').timestampTruncate(field('granularity')) + * ``` + * + * @param timestampExpression Truncate the timestamp value that is returned by this expression. + * @param granularity The granularity to truncate to. + * @param timezone The timezone to use for truncation. Valid values are from + * the TZ database (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {Expression} representing the truncated timestamp. + */ +export function timestampTruncate( + timestampExpression: Expression, + granularity: Expression, + timezone?: string | Expression +): FunctionExpression; +export function timestampTruncate( + fieldNameOrExpression: string | Expression, + granularity: TimeGranularity | Expression, + timezone?: string | Expression +): FunctionExpression { + const internalGranularity = isString(granularity) + ? valueToDefaultExpr(granularity.toLowerCase()) + : granularity; + return fieldOrExpression(fieldNameOrExpression).timestampTruncate( + internalGranularity, + timezone + ); +} + +/** + * @beta + * Creates an expression that returns the data type of the data in the specified field. + * + * @example + * ```typescript + * // Get the data type of the value in field 'title' + * type('title') + * ``` + * + * @return A new {Expression} representing the data type. + */ +export function type(fieldName: string): FunctionExpression; +/** + * @beta + * Creates an expression that returns the data type of an expression's result. + * + * @example + * ```typescript + * // Get the data type of a conditional expression + * type(conditional(exists('foo'), constant(1), constant(true))) + * ``` + * + * @return A new {Expression} representing the data type. + */ +export function type(expression: Expression): FunctionExpression; +export function type( + fieldNameOrExpression: string | Expression +): FunctionExpression { + return fieldOrExpression(fieldNameOrExpression).type(); +} + // TODO(new-expression): Add new top-level expression function definitions above this line /** diff --git a/packages/firestore/src/lite-api/pipeline-result.ts b/packages/firestore/src/lite-api/pipeline-result.ts index c352ba48338..8ed02d8cfa7 100644 --- a/packages/firestore/src/lite-api/pipeline-result.ts +++ b/packages/firestore/src/lite-api/pipeline-result.ts @@ -16,6 +16,7 @@ */ import { ObjectValue } from '../model/object_value'; +import { firestoreV1ApiClientInterfaces } from '../protos/firestore_proto_api'; import { isOptionalEqual } from '../util/misc'; import { Field, isField } from './expressions'; @@ -26,6 +27,27 @@ import { Timestamp } from './timestamp'; import { fieldPathFromArgument } from './user_data_reader'; import { AbstractUserDataWriter } from './user_data_writer'; +/** + * @beta + * Represents the results of a Firestore pipeline execution. + * + * A `PipelineSnapshot` contains zero or more {@link PipelineResult} objects + * representing the documents returned by a pipeline query. It provides methods + * to iterate over the documents and access metadata about the query results. + * + * @example + * ```typescript + * const snapshot: PipelineSnapshot = await firestore + * .pipeline() + * .collection('myCollection') + * .where(field('value').greaterThan(10)) + * .execute(); + * + * snapshot.results.forEach(doc => { + * console.log(doc.id, '=>', doc.data()); + * }); + * ``` + */ export class PipelineSnapshot { private readonly _pipeline: Pipeline; private readonly _executionTime: Timestamp | undefined; @@ -40,12 +62,15 @@ export class PipelineSnapshot { this._results = results; } - /** An array of all the results in the `PipelineSnapshot`. */ + /** + * @beta An array of all the results in the `PipelineSnapshot`. + */ get results(): PipelineResult[] { return this._results; } /** + * @beta * The time at which the pipeline producing this result is executed. * * @type {Timestamp} @@ -115,6 +140,7 @@ export class PipelineResult { } /** + * @beta * The reference of the document, if it is a document; otherwise `undefined`. */ get ref(): DocumentReference | undefined { @@ -122,6 +148,7 @@ export class PipelineResult { } /** + * @beta * The ID of the document for which this PipelineResult contains data, if it is a document; otherwise `undefined`. * * @type {string} @@ -133,6 +160,7 @@ export class PipelineResult { } /** + * @beta * The time the document was created. Undefined if this result is not a document. * * @type {Timestamp|undefined} @@ -143,6 +171,7 @@ export class PipelineResult { } /** + * @beta * The time the document was last updated (at the time the snapshot was * generated). Undefined if this result is not a document. * @@ -154,6 +183,7 @@ export class PipelineResult { } /** + * @beta * Retrieves all fields in the result as an object. * * @returns {T} An object containing all fields in the document or @@ -176,6 +206,20 @@ export class PipelineResult { } /** + * @internal + * @private + * + * Retrieves all fields in the result as a proto value. + * + * @returns An `Object` containing all fields in the result. + */ + _fieldsProto(): { [key: string]: firestoreV1ApiClientInterfaces.Value } { + // Return a cloned value to prevent manipulation of the Snapshot's data + return this._fields.clone().value.mapValue.fields!; + } + + /** + * @beta * Retrieves the field specified by `field`. * * @param {string|FieldPath|Field} field The field path diff --git a/packages/firestore/src/lite-api/pipeline-source.ts b/packages/firestore/src/lite-api/pipeline-source.ts index 3f4d62cb0be..0186ca35268 100644 --- a/packages/firestore/src/lite-api/pipeline-source.ts +++ b/packages/firestore/src/lite-api/pipeline-source.ts @@ -43,8 +43,12 @@ import { import { UserDataReader, UserDataSource } from './user_data_reader'; /** - * Represents the source of a Firestore {@link Pipeline}. * @beta + * Provides the entry point for defining the data source of a Firestore {@link Pipeline}. + * + * Use the methods of this class (e.g., {@link PipelineSource#collection}, {@link PipelineSource#collectionGroup}, + * {@link PipelineSource#database}, or {@link PipelineSource#documents}) to specify the initial data + * for your pipeline, such as a collection, a collection group, the entire database, or a set of specific documents. */ export class PipelineSource { /** @@ -65,11 +69,13 @@ export class PipelineSource { ) {} /** + * @beta * Returns all documents from the entire collection. The collection can be nested. * @param collection - Name or reference to the collection that will be used as the Pipeline source. */ collection(collection: string | CollectionReference): PipelineType; /** + * @beta * Returns all documents from the entire collection. The collection can be nested. * @param options - Options defining how this CollectionStage is evaluated. */ @@ -115,11 +121,13 @@ export class PipelineSource { } /** + * @beta * 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; /** + * @beta * Returns all documents from a collection ID regardless of the parent. * @param options - Options defining how this CollectionGroupStage is evaluated. */ @@ -153,10 +161,12 @@ export class PipelineSource { } /** + * @beta * Returns all documents from the entire database. */ database(): PipelineType; /** + * @beta * Returns all documents from the entire database. * @param options - Options defining how a DatabaseStage is evaluated. */ @@ -181,6 +191,7 @@ export class PipelineSource { } /** + * @beta * Set the pipeline's source to the documents specified by the given paths and DocumentReferences. * * @param docs An array of paths and DocumentReferences specifying the individual documents that will be the source of this pipeline. @@ -191,6 +202,7 @@ export class PipelineSource { documents(docs: Array): PipelineType; /** + * @beta * Set the pipeline's source to the documents specified by the given paths and DocumentReferences. * * @param options - Options defining how this DocumentsStage is evaluated. @@ -237,6 +249,7 @@ export class PipelineSource { } /** + * @beta * Convert the given Query into an equivalent Pipeline. * * @param query A Query to be converted into a Pipeline. diff --git a/packages/firestore/src/lite-api/pipeline.ts b/packages/firestore/src/lite-api/pipeline.ts index f4aae07b828..470916f6b4d 100644 --- a/packages/firestore/src/lite-api/pipeline.ts +++ b/packages/firestore/src/lite-api/pipeline.ts @@ -152,6 +152,7 @@ export class Pipeline implements ProtoSerializable { ) {} /** + * @beta * Adds new fields to outputs from previous stages. * * This stage allows you to compute values on-the-fly based on existing data from previous @@ -180,6 +181,7 @@ export class Pipeline implements ProtoSerializable { */ addFields(field: Selectable, ...additionalFields: Selectable[]): Pipeline; /** + * @beta * Adds new fields to outputs from previous stages. * * This stage allows you to compute values on-the-fly based on existing data from previous @@ -239,6 +241,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * Remove fields from outputs of previous stages. * * Example: @@ -261,6 +264,7 @@ export class Pipeline implements ProtoSerializable { ...additionalFields: Array ): Pipeline; /** + * @beta * Remove fields from outputs of previous stages. * * Example: @@ -311,6 +315,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * 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: @@ -348,6 +353,7 @@ export class Pipeline implements ProtoSerializable { ...additionalSelections: Array ): Pipeline; /** + * @beta * 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: @@ -413,6 +419,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * Filters the documents from previous stages to only include those matching the specified {@link * BooleanExpression}. * @@ -445,6 +452,7 @@ export class Pipeline implements ProtoSerializable { */ where(condition: BooleanExpression): Pipeline; /** + * @beta * Filters the documents from previous stages to only include those matching the specified {@link * BooleanExpression}. * @@ -499,6 +507,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * 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 @@ -520,6 +529,7 @@ export class Pipeline implements ProtoSerializable { */ offset(offset: number): Pipeline; /** + * @beta * 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 @@ -568,6 +578,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * 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 @@ -594,6 +605,7 @@ export class Pipeline implements ProtoSerializable { */ limit(limit: number): Pipeline; /** + * @beta * 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 @@ -642,6 +654,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * 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 @@ -674,6 +687,7 @@ export class Pipeline implements ProtoSerializable { ...additionalGroups: Array ): Pipeline; /** + * @beta * 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 @@ -732,6 +746,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * Performs aggregation operations on the documents from previous stages. * *

This stage allows you to calculate aggregate values over a set of documents. You define the @@ -760,6 +775,7 @@ export class Pipeline implements ProtoSerializable { ...additionalAccumulators: AliasedAggregate[] ): Pipeline; /** + * @beta * Performs optionally grouped aggregation operations on the documents from previous stages. * *

This stage allows you to calculate aggregate values over a set of documents, optionally @@ -832,6 +848,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * 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`. @@ -890,6 +907,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * 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 @@ -916,6 +934,7 @@ export class Pipeline implements ProtoSerializable { */ sort(ordering: Ordering, ...additionalOrderings: Ordering[]): Pipeline; /** + * @beta * 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 @@ -966,6 +985,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * Fully overwrites all fields in a document with those coming from a nested map. * *

This stage allows you to emit a map value as a document. Each key of the map becomes a field @@ -998,6 +1018,7 @@ export class Pipeline implements ProtoSerializable { */ replaceWith(fieldName: string): Pipeline; /** + * @beta * 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 @@ -1035,6 +1056,7 @@ export class Pipeline implements ProtoSerializable { */ replaceWith(expr: Expression): Pipeline; /** + * @beta * 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 @@ -1101,6 +1123,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * Performs a pseudo-random sampling of the documents from the previous stage. * *

This stage will filter documents pseudo-randomly. The parameter specifies how number of @@ -1120,6 +1143,7 @@ export class Pipeline implements ProtoSerializable { sample(documents: number): Pipeline; /** + * @beta * Performs a pseudo-random sampling of the documents from the previous stage. * *

This stage will filter documents pseudo-randomly. The 'options' parameter specifies how @@ -1171,6 +1195,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * Performs union of all documents from two pipelines, including duplicates. * *

This stage will pass through documents from previous stage, and also pass through documents @@ -1190,6 +1215,7 @@ export class Pipeline implements ProtoSerializable { */ union(other: Pipeline): Pipeline; /** + * @beta * Performs union of all documents from two pipelines, including duplicates. * *

This stage will pass through documents from previous stage, and also pass through documents @@ -1235,6 +1261,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * 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 @@ -1268,6 +1295,7 @@ export class Pipeline implements ProtoSerializable { */ unnest(selectable: Selectable, indexField?: string): Pipeline; /** + * @beta * 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 @@ -1342,6 +1370,7 @@ export class Pipeline implements ProtoSerializable { } /** + * @beta * Adds a raw stage to the pipeline. * *

This method provides a flexible way to extend the pipeline's functionality by adding custom diff --git a/packages/firestore/src/lite-api/pipeline_impl.ts b/packages/firestore/src/lite-api/pipeline_impl.ts index 397e27bc1b4..dbfe96a0991 100644 --- a/packages/firestore/src/lite-api/pipeline_impl.ts +++ b/packages/firestore/src/lite-api/pipeline_impl.ts @@ -37,11 +37,21 @@ import { declare module './database' { interface Firestore { + /** + * @beta + * Creates and returns a new PipelineSource, which allows specifying the source stage of a {@link Pipeline}. + * + * @example + * ``` + * let myPipeline: Pipeline = firestore.pipeline().collection('books'); + * ``` + */ pipeline(): PipelineSource; } } /** + * @beta * Executes this pipeline and returns a Promise to represent the asynchronous operation. * * The returned Promise can be used to track the progress of the pipeline execution @@ -120,6 +130,15 @@ export function execute(pipeline: Pipeline): Promise { }); } +/** + * @beta + * Creates and returns a new PipelineSource, which allows specifying the source stage of a {@link Pipeline}. + * + * @example + * ``` + * let myPipeline: Pipeline = firestore.pipeline().collection('books'); + * ``` + */ Firestore.prototype.pipeline = function (): PipelineSource { const userDataWriter = new LiteUserDataWriter(this); const userDataReader = newUserDataReader(this); diff --git a/packages/firestore/src/lite-api/snapshot.ts b/packages/firestore/src/lite-api/snapshot.ts index ba7b08cf9dd..99474bda5f5 100644 --- a/packages/firestore/src/lite-api/snapshot.ts +++ b/packages/firestore/src/lite-api/snapshot.ts @@ -19,6 +19,7 @@ import { getModularInstance } from '@firebase/util'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { firestoreV1ApiClientInterfaces } from '../protos/firestore_proto_api'; import { arrayEquals } from '../util/misc'; import { Firestore } from './database'; @@ -365,6 +366,23 @@ export class DocumentSnapshot< } } + /** + * @internal + * @private + * + * Retrieves all fields in the document as a proto Value. Returns `undefined` if + * the document doesn't exist. + * + * @returns An `Object` containing all fields in the document or `undefined` + * if the document doesn't exist. + */ + _fieldsProto(): + | { [key: string]: firestoreV1ApiClientInterfaces.Value } + | undefined { + // Return a cloned value to prevent manipulation of the Snapshot's data + return this._document?.data.clone().value.mapValue.fields ?? undefined; + } + /** * Retrieves the field specified by `fieldPath`. Returns `undefined` if the * document or field doesn't exist. diff --git a/packages/firestore/src/platform/browser/webchannel_connection.ts b/packages/firestore/src/platform/browser/webchannel_connection.ts index 56f57aa9595..32673669b4d 100644 --- a/packages/firestore/src/platform/browser/webchannel_connection.ts +++ b/packages/firestore/src/platform/browser/webchannel_connection.ts @@ -16,7 +16,7 @@ */ import { - createWebChannelTransport, + createWebChannelTransport as internalCreateWebChannelTransport, ErrorCode, EventType, WebChannel, @@ -27,7 +27,8 @@ import { EventTarget, StatEvent, Event, - Stat + Stat, + WebChannelTransport } from '@firebase/webchannel-wrapper/webchannel-blob'; import { Token } from '../../api/credentials'; @@ -181,7 +182,7 @@ export class WebChannelConnection extends RestConnection { rpcName, '/channel' ]; - const webchannelTransport = createWebChannelTransport(); + const webchannelTransport = this.createWebChannelTransport(); const requestStats = getStatEventTarget(); const request: WebChannelOptions = { // Required for backend stickiness, routing behavior is based on this @@ -460,4 +461,29 @@ export class WebChannelConnection extends RestConnection { instance => instance === webChannel ); } + + /** + * Modifies the headers for a request, adding the api key if present, + * and then calling super.modifyHeadersForRequest + */ + protected modifyHeadersForRequest( + headers: StringMap, + authToken: Token | null, + appCheckToken: Token | null + ): void { + super.modifyHeadersForRequest(headers, authToken, appCheckToken); + + // For web channel streams, we want to send the api key in the headers. + if (this.databaseInfo.apiKey) { + headers['x-goog-api-key'] = this.databaseInfo.apiKey; + } + } + + /** + * Wrapped for mocking. + * @protected + */ + protected createWebChannelTransport(): WebChannelTransport { + return internalCreateWebChannelTransport(); + } } diff --git a/packages/firestore/src/platform/node/grpc_connection.ts b/packages/firestore/src/platform/node/grpc_connection.ts index d50a3149416..79ccb3ddf3c 100644 --- a/packages/firestore/src/platform/node/grpc_connection.ts +++ b/packages/firestore/src/platform/node/grpc_connection.ts @@ -44,7 +44,8 @@ function createMetadata( databasePath: string, authToken: Token | null, appCheckToken: Token | null, - appId: string + appId: string, + apiKey: string | undefined ): grpc.Metadata { hardAssert( authToken === null || authToken.type === 'OAuth', @@ -69,6 +70,9 @@ function createMetadata( // 11 from Google3. metadata.set('Google-Cloud-Resource-Prefix', databasePath); metadata.set('x-goog-request-params', databasePath); + if (apiKey) { + metadata.set('X-Goog-Api-Key', apiKey); + } return metadata; } @@ -100,7 +104,8 @@ export class GrpcConnection implements Connection { this.databasePath = `projects/${databaseInfo.databaseId.projectId}/databases/${databaseInfo.databaseId.database}`; } - private ensureActiveStub(): GeneratedGrpcStub { + /** made protected for testing */ + protected ensureActiveStub(): GeneratedGrpcStub { if (!this.cachedStub) { logDebug(LOG_TAG, 'Creating Firestore stub.'); const credentials = this.databaseInfo.ssl @@ -127,7 +132,8 @@ export class GrpcConnection implements Connection { this.databasePath, authToken, appCheckToken, - this.databaseInfo.appId + this.databaseInfo.appId, + this.databaseInfo.apiKey ); const jsonRequest = { database: this.databasePath, ...request }; @@ -187,7 +193,8 @@ export class GrpcConnection implements Connection { this.databasePath, authToken, appCheckToken, - this.databaseInfo.appId + this.databaseInfo.appId, + this.databaseInfo.apiKey ); const jsonRequest = { ...request, database: this.databasePath }; const stream = stub[rpcName](jsonRequest, metadata); @@ -239,7 +246,8 @@ export class GrpcConnection implements Connection { this.databasePath, authToken, appCheckToken, - this.databaseInfo.appId + this.databaseInfo.appId, + this.databaseInfo.apiKey ); const grpcStream = stub[rpcName](metadata); diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index 081b8cf5c9a..4c84fca96ec 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -263,19 +263,17 @@ export async function invokeExecutePipeline( ); const result: PipelineStreamElement[] = []; - response - .filter(proto => !!proto.results) - .forEach(proto => { - if (proto.results!.length === 0) { - result.push(fromPipelineResponse(datastoreImpl.serializer, proto)); - } else { - return proto.results!.forEach(document => - result.push( - fromPipelineResponse(datastoreImpl.serializer, proto, document) - ) - ); - } - }); + response.forEach(proto => { + if (!proto.results || proto.results!.length === 0) { + result.push(fromPipelineResponse(datastoreImpl.serializer, proto)); + } else { + return proto.results!.forEach(document => + result.push( + fromPipelineResponse(datastoreImpl.serializer, proto, document) + ) + ); + } + }); return result; } diff --git a/packages/firestore/src/remote/internal_serializer.ts b/packages/firestore/src/remote/internal_serializer.ts index 29a68620efc..1e759dab814 100644 --- a/packages/firestore/src/remote/internal_serializer.ts +++ b/packages/firestore/src/remote/internal_serializer.ts @@ -18,14 +18,23 @@ import { ensureFirestoreConfigured, Firestore } from '../api/database'; import { AggregateImpl } from '../core/aggregate'; import { queryToAggregateTarget, queryToTarget } from '../core/query'; +import { + StructuredPipeline, + StructuredPipelineOptions +} from '../core/structured_pipeline'; import { AggregateSpec } from '../lite-api/aggregate_types'; import { getDatastore } from '../lite-api/components'; import { Pipeline } from '../lite-api/pipeline'; import { Query } from '../lite-api/reference'; +import { ExecutePipelineRequest as ProtoExecutePipelineRequest } from '../protos/firestore_proto_api'; import { cast } from '../util/input_validation'; import { mapToArray } from '../util/obj'; -import { toQueryTarget, toRunAggregationQueryRequest } from './serializer'; +import { + getEncodedDatabaseId, + toQueryTarget, + toRunAggregationQueryRequest +} from './serializer'; /** * @internal @@ -112,5 +121,15 @@ export function _internalPipelineToExecutePipelineRequestProto( if (serializer === undefined) { return null; } - return pipeline._toProto(serializer); + + const structuredPipeline = new StructuredPipeline( + pipeline, + new StructuredPipelineOptions() + ); + const executePipelineRequest: ProtoExecutePipelineRequest = { + database: getEncodedDatabaseId(serializer), + structuredPipeline: structuredPipeline._toProto(serializer) + }; + + return executePipelineRequest; } diff --git a/packages/firestore/src/remote/rest_connection.ts b/packages/firestore/src/remote/rest_connection.ts index 7469d8f45ff..83b0c572f9a 100644 --- a/packages/firestore/src/remote/rest_connection.ts +++ b/packages/firestore/src/remote/rest_connection.ts @@ -71,7 +71,7 @@ export abstract class RestConnection implements Connection { return false; } - constructor(private readonly databaseInfo: DatabaseInfo) { + constructor(protected readonly databaseInfo: DatabaseInfo) { this.databaseId = databaseInfo.databaseId; const proto = databaseInfo.ssl ? 'https' : 'http'; const projectId = encodeURIComponent(this.databaseId.projectId); @@ -194,13 +194,17 @@ export abstract class RestConnection implements Connection { _forwardCredentials: boolean ): Promise; - private makeUrl(rpcName: string, path: string): string { + protected makeUrl(rpcName: string, path: string): string { const urlRpcName = RPC_NAME_URL_MAPPING[rpcName]; debugAssert( urlRpcName !== undefined, 'Unknown REST mapping for: ' + rpcName ); - return `${this.baseUrl}/${RPC_URL_VERSION}/${path}:${urlRpcName}`; + let url = `${this.baseUrl}/${RPC_URL_VERSION}/${path}:${urlRpcName}`; + if (this.databaseInfo.apiKey) { + url = `${url}?key=${encodeURIComponent(this.databaseInfo.apiKey)}`; + } + return url; } /** diff --git a/packages/firestore/src/util/pipeline_util.ts b/packages/firestore/src/util/pipeline_util.ts index 0bc1906361c..a7400828fd9 100644 --- a/packages/firestore/src/util/pipeline_util.ts +++ b/packages/firestore/src/util/pipeline_util.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { vector } from '../api'; +import { FirestoreError, vector } from '../api'; import { _constant, AggregateFunction, @@ -31,6 +31,7 @@ import { } from '../lite-api/expressions'; import { VectorValue } from '../lite-api/vector_value'; +import { fail } from './assert'; import { isPlainObject } from './input_validation'; import { isFirestoreValue } from './proto'; import { isString } from './types'; @@ -40,13 +41,29 @@ export function selectablesToMap( ): Map { const result = new Map(); for (const selectable of selectables) { + let alias: string; + let expression: Expression; if (typeof selectable === 'string') { - result.set(selectable as string, field(selectable)); + alias = selectable as string; + expression = field(selectable); } else if (selectable instanceof Field) { - result.set(selectable.alias, selectable.expr); + alias = selectable.alias; + expression = selectable.expr; } else if (selectable instanceof AliasedExpression) { - result.set(selectable.alias, selectable.expr); + alias = selectable.alias; + expression = selectable.expr; + } else { + fail(0x5319, '`selectable` has an unsupported type', { selectable }); } + + if (result.get(alias) !== undefined) { + throw new FirestoreError( + 'invalid-argument', + `Duplicate alias or field '${alias}'` + ); + } + + result.set(alias, expression); } return result; } @@ -56,6 +73,13 @@ export function aliasedAggregateToMap( ): Map { return aliasedAggregatees.reduce( (map: Map, selectable: AliasedAggregate) => { + if (map.get(selectable.alias) !== undefined) { + throw new FirestoreError( + 'invalid-argument', + `Duplicate alias or field '${selectable.alias}'` + ); + } + map.set(selectable.alias, selectable.aggregate as AggregateFunction); return map; }, diff --git a/packages/firestore/test/integration/api/aggregation.test.ts b/packages/firestore/test/integration/api/aggregation.test.ts index ba44dddf514..0852cb3593b 100644 --- a/packages/firestore/test/integration/api/aggregation.test.ts +++ b/packages/firestore/test/integration/api/aggregation.test.ts @@ -17,6 +17,7 @@ import { expect } from 'chai'; +import { it } from '../../util/mocha_extensions'; import { collection, collectionGroup, @@ -42,7 +43,6 @@ import { withTestCollection, withTestDb } from '../util/helpers'; -import { USE_EMULATOR } from '../util/settings'; apiDescribe('Count queries', persistence => { it('can run count query getCountFromServer', () => { @@ -150,7 +150,7 @@ apiDescribe('Count queries', persistence => { // production, since the Firestore Emulator does not require index creation // and will, therefore, never fail in this situation. // eslint-disable-next-line no-restricted-properties - (USE_EMULATOR ? it.skip : it)( + it.skipEnterprise.skipEmulator( 'getCountFromServer error message contains console link if missing index', () => { return withEmptyTestCollection(persistence, async coll => { @@ -362,7 +362,7 @@ apiDescribe('Aggregation queries', persistence => { // production, since the Firestore Emulator does not require index creation // and will, therefore, never fail in this situation. // eslint-disable-next-line no-restricted-properties - (USE_EMULATOR ? it.skip : it)( + it.skipEmulator.skipEnterprise( 'getAggregateFromServer error message contains console link good if missing index', () => { return withEmptyTestCollection(persistence, async coll => { @@ -480,26 +480,29 @@ apiDescribe('Aggregation queries - sum / average', persistence => { }); }); - it('fails when exceeding the max (5) aggregations using getAggregationFromServer', () => { - const testDocs = { - a: { author: 'authorA', title: 'titleA', pages: 100 }, - b: { author: 'authorB', title: 'titleB', pages: 50 } - }; - return withTestCollection(persistence, testDocs, async coll => { - const promise = getAggregateFromServer(coll, { - totalPages: sum('pages'), - averagePages: average('pages'), - count: count(), - totalPagesX: sum('pages'), - averagePagesY: average('pages'), - countZ: count() + it.skipEnterprise( + 'fails when exceeding the max (5) aggregations using getAggregationFromServer', + () => { + const testDocs = { + a: { author: 'authorA', title: 'titleA', pages: 100 }, + b: { author: 'authorB', title: 'titleB', pages: 50 } + }; + return withTestCollection(persistence, testDocs, async coll => { + const promise = getAggregateFromServer(coll, { + totalPages: sum('pages'), + averagePages: average('pages'), + count: count(), + totalPagesX: sum('pages'), + averagePagesY: average('pages'), + countZ: count() + }); + + await expect(promise).to.eventually.be.rejectedWith( + /maximum number of aggregations/ + ); }); - - await expect(promise).to.eventually.be.rejectedWith( - /maximum number of aggregations/ - ); - }); - }); + } + ); it('returns undefined when getting the result of an unrequested aggregation', () => { const testDocs = { @@ -940,47 +943,50 @@ apiDescribe('Aggregation queries - sum / average', persistence => { }); }); - it('performs sum over a result set of zero documents using getAggregationFromServer', () => { - const testDocs = { - a: { - author: 'authorA', - title: 'titleA', - pages: 100, - year: 1980, - rating: 5 - }, - b: { - author: 'authorB', - title: 'titleB', - pages: 50, - year: 2020, - rating: 4 - }, - c: { - author: 'authorC', - title: 'titleC', - pages: 100, - year: 1980, - rating: 3 - }, - d: { - author: 'authorD', - title: 'titleD', - pages: 50, - year: 2020, - rating: 0 - } - }; - return withTestCollection(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer( - query(coll, where('pages', '>', 200)), - { - totalPages: sum('pages') + it.skipEnterprise( + 'performs sum over a result set of zero documents using getAggregationFromServer', + () => { + const testDocs = { + a: { + author: 'authorA', + title: 'titleA', + pages: 100, + year: 1980, + rating: 5 + }, + b: { + author: 'authorB', + title: 'titleB', + pages: 50, + year: 2020, + rating: 4 + }, + c: { + author: 'authorC', + title: 'titleC', + pages: 100, + year: 1980, + rating: 3 + }, + d: { + author: 'authorD', + title: 'titleD', + pages: 50, + year: 2020, + rating: 0 } - ); - expect(snapshot.data().totalPages).to.equal(0); - }); - }); + }; + return withTestCollection(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer( + query(coll, where('pages', '>', 200)), + { + totalPages: sum('pages') + } + ); + expect(snapshot.data().totalPages).to.equal(0); + }); + } + ); it('performs sum only on numeric fields using getAggregationFromServer', () => { const testDocs = { diff --git a/packages/firestore/test/integration/api/composite_index_query.test.ts b/packages/firestore/test/integration/api/composite_index_query.test.ts index d08cc77bde9..0dbc765788c 100644 --- a/packages/firestore/test/integration/api/composite_index_query.test.ts +++ b/packages/firestore/test/integration/api/composite_index_query.test.ts @@ -17,6 +17,7 @@ import { expect } from 'chai'; +import { it } from '../../util/mocha_extensions'; import { CompositeIndexTestHelper } from '../util/composite_index_test_helper'; import { where, @@ -196,27 +197,33 @@ apiDescribe('Composite Index Queries', persistence => { }); }); - it('performs aggregations on documents with all aggregated fields using getAggregationFromServer', () => { - const testDocs = { - a: { author: 'authorA', title: 'titleA', pages: 100, year: 1980 }, - b: { author: 'authorB', title: 'titleB', pages: 50, year: 2020 }, - c: { author: 'authorC', title: 'titleC', pages: 150, year: 2021 }, - d: { author: 'authorD', title: 'titleD', pages: 50 } - }; - const testHelper = new CompositeIndexTestHelper(); - return testHelper.withTestDocs(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer(testHelper.query(coll), { - totalPages: sum('pages'), - averagePages: average('pages'), - averageYear: average('year'), - count: count() + it.skipEmulator.skipEnterprise( + 'performs aggregations on documents with all aggregated fields using getAggregationFromServer', + () => { + const testDocs = { + a: { author: 'authorA', title: 'titleA', pages: 100, year: 1980 }, + b: { author: 'authorB', title: 'titleB', pages: 50, year: 2020 }, + c: { author: 'authorC', title: 'titleC', pages: 150, year: 2021 }, + d: { author: 'authorD', title: 'titleD', pages: 50 } + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer( + testHelper.query(coll), + { + totalPages: sum('pages'), + averagePages: average('pages'), + averageYear: average('year'), + count: count() + } + ); + expect(snapshot.data().totalPages).to.equal(300); + expect(snapshot.data().averagePages).to.equal(100); + expect(snapshot.data().averageYear).to.equal(2007); + expect(snapshot.data().count).to.equal(3); }); - expect(snapshot.data().totalPages).to.equal(300); - expect(snapshot.data().averagePages).to.equal(100); - expect(snapshot.data().averageYear).to.equal(2007); - expect(snapshot.data().count).to.equal(3); - }); - }); + } + ); it('performs aggregates on multiple fields where one aggregate could cause short-circuit due to NaN using getAggregationFromServer', () => { const testDocs = { @@ -262,56 +269,62 @@ apiDescribe('Composite Index Queries', persistence => { }); }); - it('performs aggregates when using `array-contains-any` operator getAggregationFromServer', () => { - const testDocs = { - a: { - author: 'authorA', - title: 'titleA', - pages: 100, - year: 1980, - rating: [5, 1000] - }, - b: { - author: 'authorB', - title: 'titleB', - pages: 50, - year: 2020, - rating: [4] - }, - c: { - author: 'authorC', - title: 'titleC', - pages: 100, - year: 1980, - rating: [2222, 3] - }, - d: { - author: 'authorD', - title: 'titleD', - pages: 50, - year: 2020, - rating: [0] - } - }; - const testHelper = new CompositeIndexTestHelper(); - return testHelper.withTestDocs(persistence, testDocs, async coll => { - const snapshot = await getAggregateFromServer( - testHelper.query(coll, where('rating', 'array-contains-any', [5, 3])), - { - totalRating: sum('rating'), - averageRating: average('rating'), - totalPages: sum('pages'), - averagePages: average('pages'), - countOfDocs: count() + it.skipEnterprise( + 'performs aggregates when using `array-contains-any` operator getAggregationFromServer', + () => { + const testDocs = { + a: { + author: 'authorA', + title: 'titleA', + pages: 100, + year: 1980, + rating: [5, 1000] + }, + b: { + author: 'authorB', + title: 'titleB', + pages: 50, + year: 2020, + rating: [4] + }, + c: { + author: 'authorC', + title: 'titleC', + pages: 100, + year: 1980, + rating: [2222, 3] + }, + d: { + author: 'authorD', + title: 'titleD', + pages: 50, + year: 2020, + rating: [0] } - ); - expect(snapshot.data().totalRating).to.equal(0); - expect(snapshot.data().averageRating).to.be.null; - expect(snapshot.data().totalPages).to.equal(200); - expect(snapshot.data().averagePages).to.equal(100); - expect(snapshot.data().countOfDocs).to.equal(2); - }); - }); + }; + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestDocs(persistence, testDocs, async coll => { + const snapshot = await getAggregateFromServer( + testHelper.query( + coll, + where('rating', 'array-contains-any', [5, 3]) + ), + { + totalRating: sum('rating'), + averageRating: average('rating'), + totalPages: sum('pages'), + averagePages: average('pages'), + countOfDocs: count() + } + ); + expect(snapshot.data().totalRating).to.equal(0); + expect(snapshot.data().averageRating).to.be.null; + expect(snapshot.data().totalPages).to.equal(200); + expect(snapshot.data().averagePages).to.equal(100); + expect(snapshot.data().countOfDocs).to.equal(2); + }); + } + ); }); describe('Multiple Inequality', () => { @@ -950,18 +963,24 @@ apiDescribe('Composite Index Queries', persistence => { }); }); - it('inequality query will reject if document key appears only in equality filter', () => { - const testHelper = new CompositeIndexTestHelper(); - return testHelper.withTestCollection(persistence, async coll => { - const query_ = testHelper.query( - coll, - where('key', '!=', 42), - where(documentId(), '==', 'doc1') - ); - await expect(testHelper.getDocs(query_)).to.be.eventually.rejectedWith( - 'Equality on key is not allowed if there are other inequality fields and key does not appear in inequalities.' - ); - }); - }); + // + it.skipEnterprise( + 'inequality query will reject if document key appears only in equality filter', + () => { + const testHelper = new CompositeIndexTestHelper(); + return testHelper.withTestCollection(persistence, async coll => { + const query_ = testHelper.query( + coll, + where('key', '!=', 42), + where(documentId(), '==', 'doc1') + ); + await expect( + testHelper.getDocs(query_) + ).to.be.eventually.rejectedWith( + 'Equality on key is not allowed if there are other inequality fields and key does not appear in inequalities.' + ); + }); + } + ); }); }); diff --git a/packages/firestore/test/integration/api/console.test.ts b/packages/firestore/test/integration/api/console.test.ts new file mode 100644 index 00000000000..0083c76c01f --- /dev/null +++ b/packages/firestore/test/integration/api/console.test.ts @@ -0,0 +1,51 @@ +import { expect } from 'chai'; + +import { + count, + setDoc, + getAggregateFromServer, + getDoc, + average +} from '../util/firebase_export'; +import { apiDescribe, withTestCollection, withTestDoc } from '../util/helpers'; + +apiDescribe('console support', persistence => { + it('supports DocumentSnapshot serialization to proto', async () => { + await withTestDoc(persistence, async (docRef, firestore) => { + await setDoc(docRef, { foo: 3, bar: 3.5 }); + const doc = await getDoc(docRef); + expect(doc._fieldsProto()).to.deep.equal({ + 'foo': { + 'integerValue': '3' + }, + 'bar': { + 'doubleValue': 3.5 + } + }); + }); + }); + + it('supports AggregateSnapshot serialization to proto', async () => { + await withTestCollection( + persistence, + { + 1: { foo: 1 }, + 2: { foo: 1 } + }, + async collRef => { + const doc = await getAggregateFromServer(collRef, { + count: count(), + avg: average('foo') + }); + expect(doc._fieldsProto()).to.deep.equal({ + 'count': { + 'integerValue': '2' + }, + 'avg': { + 'doubleValue': 1.0 + } + }); + } + ); + }); +}); diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index b63c03a4f62..05a29680f9e 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -20,6 +20,7 @@ import { Deferred, isNode } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import { it } from '../../util/mocha_extensions'; import { EventsAccumulator } from '../util/events_accumulator'; import { addDoc, @@ -2733,58 +2734,10 @@ apiDescribe('Database', persistence => { }); }); - it('snapshot listener sorts filtered query by DocumentId same way as get query', async () => { - const testDocs = { - 'A': { a: 1 }, - 'a': { a: 1 }, - 'Aa': { a: 1 }, - '7': { a: 1 }, - '12': { a: 1 }, - '__id7__': { a: 1 }, - '__id12__': { a: 1 }, - '__id-2__': { a: 1 }, - '_id1__': { a: 1 }, - '__id1_': { a: 1 }, - '__id': { a: 1 }, - // largest long numbers - '__id9223372036854775807__': { a: 1 }, - '__id9223372036854775806__': { a: 1 }, - // smallest long numbers - '__id-9223372036854775808__': { a: 1 }, - '__id-9223372036854775807__': { a: 1 } - }; - - return withTestCollection(persistence, testDocs, async collectionRef => { - const filteredQuery = query( - collectionRef, - orderBy(documentId()), - where(documentId(), '>', '__id7__'), - where(documentId(), '<=', 'Aa') - ); - const expectedDocs = [ - '__id12__', - '__id9223372036854775806__', - '__id9223372036854775807__', - '12', - '7', - 'A', - 'Aa' - ]; - - const getSnapshot = await getDocsFromServer(filteredQuery); - expect(toIds(getSnapshot)).to.deep.equal(expectedDocs); - - const storeEvent = new EventsAccumulator(); - const unsubscribe = onSnapshot(filteredQuery, storeEvent.storeEvent); - const watchSnapshot = await storeEvent.awaitEvent(); - expect(toIds(watchSnapshot)).to.deep.equal(expectedDocs); - unsubscribe(); - }); - }); - - // eslint-disable-next-line no-restricted-properties - (persistence.gc === 'lru' ? describe : describe.skip)('offline', () => { - it('SDK orders query the same way online and offline', async () => { + // Enterprise does not sort numeric IDs before string + it.skipEnterprise( + 'snapshot listener sorts filtered query by DocumentId same way as get query', + async () => { const testDocs = { 'A': { a: 1 }, 'a': { a: 1 }, @@ -2809,37 +2762,13 @@ apiDescribe('Database', persistence => { persistence, testDocs, async collectionRef => { - const orderedQuery = query(collectionRef, orderBy(documentId())); - let expectedDocs = [ - '__id-9223372036854775808__', - '__id-9223372036854775807__', - '__id-2__', - '__id7__', - '__id12__', - '__id9223372036854775806__', - '__id9223372036854775807__', - '12', - '7', - 'A', - 'Aa', - '__id', - '__id1_', - '_id1__', - 'a' - ]; - await checkOnlineAndOfflineResultsMatch( - collectionRef, - orderedQuery, - ...expectedDocs - ); - const filteredQuery = query( collectionRef, orderBy(documentId()), where(documentId(), '>', '__id7__'), where(documentId(), '<=', 'Aa') ); - expectedDocs = [ + const expectedDocs = [ '__id12__', '__id9223372036854775806__', '__id9223372036854775807__', @@ -2848,14 +2777,100 @@ apiDescribe('Database', persistence => { 'A', 'Aa' ]; - await checkOnlineAndOfflineResultsMatch( - collectionRef, + + const getSnapshot = await getDocsFromServer(filteredQuery); + expect(toIds(getSnapshot)).to.deep.equal(expectedDocs); + + const storeEvent = new EventsAccumulator(); + const unsubscribe = onSnapshot( filteredQuery, - ...expectedDocs + storeEvent.storeEvent ); + const watchSnapshot = await storeEvent.awaitEvent(); + expect(toIds(watchSnapshot)).to.deep.equal(expectedDocs); + unsubscribe(); } ); - }); + } + ); + + // eslint-disable-next-line no-restricted-properties + (persistence.gc === 'lru' ? describe : describe.skip)('offline', () => { + it.skipEnterprise( + 'SDK orders query the same way online and offline', + async () => { + const testDocs = { + 'A': { a: 1 }, + 'a': { a: 1 }, + 'Aa': { a: 1 }, + '7': { a: 1 }, + '12': { a: 1 }, + '__id7__': { a: 1 }, + '__id12__': { a: 1 }, + '__id-2__': { a: 1 }, + '_id1__': { a: 1 }, + '__id1_': { a: 1 }, + '__id': { a: 1 }, + // largest long numbers + '__id9223372036854775807__': { a: 1 }, + '__id9223372036854775806__': { a: 1 }, + // smallest long numbers + '__id-9223372036854775808__': { a: 1 }, + '__id-9223372036854775807__': { a: 1 } + }; + + return withTestCollection( + persistence, + testDocs, + async collectionRef => { + const orderedQuery = query(collectionRef, orderBy(documentId())); + let expectedDocs = [ + '__id-9223372036854775808__', + '__id-9223372036854775807__', + '__id-2__', + '__id7__', + '__id12__', + '__id9223372036854775806__', + '__id9223372036854775807__', + '12', + '7', + 'A', + 'Aa', + '__id', + '__id1_', + '_id1__', + 'a' + ]; + await checkOnlineAndOfflineResultsMatch( + collectionRef, + orderedQuery, + ...expectedDocs + ); + + const filteredQuery = query( + collectionRef, + orderBy(documentId()), + where(documentId(), '>', '__id7__'), + where(documentId(), '<=', 'Aa') + ); + expectedDocs = [ + '__id12__', + '__id9223372036854775806__', + '__id9223372036854775807__', + '12', + '7', + 'A', + 'Aa' + ]; + await checkOnlineAndOfflineResultsMatch( + collectionRef, + filteredQuery, + ...expectedDocs + ); + } + ); + } + ); }); }); diff --git a/packages/firestore/test/integration/api/pipeline.test.ts b/packages/firestore/test/integration/api/pipeline.test.ts index aab69008254..dea86814219 100644 --- a/packages/firestore/test/integration/api/pipeline.test.ts +++ b/packages/firestore/test/integration/api/pipeline.test.ts @@ -48,7 +48,6 @@ import { pipelineResultEqual, sum, descending, - isNan, map, execute, add, @@ -97,9 +96,6 @@ import { ifError, trim, isAbsent, - isNull, - isNotNull, - isNotNan, timestampSubtract, mapRemove, mapMerge, @@ -136,16 +132,18 @@ import { log, sqrt, stringReverse, - len as length, + length, abs, concat, - error, currentTimestamp, ifAbsent, join, log10, arraySum, - PipelineSnapshot + PipelineSnapshot, + timestampTruncate, + split, + type } from '../util/pipeline_export'; use(chaiAsPromised); @@ -154,9 +152,7 @@ setLogLevel('debug'); const timestampDeltaMS = 1000; -(process.env.FIRESTORE_TARGET_DB_ID === 'enterprise' - ? apiDescribe.only - : apiDescribe.skip)('Pipelines', persistence => { +apiDescribe.skipClassic('Pipelines', persistence => { addEqualityMatcher(); let firestore: Firestore; @@ -345,19 +341,129 @@ const timestampDeltaMS = 1000; }); describe('console support', () => { - it('supports internal serialization to proto', async () => { + it('supports pipeline query serialization to proto', async () => { + // Perform the same test as the console 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') - ); + .collection('customers') + .where(field('country').equal('United Kingdom')); const proto = _internalPipelineToExecutePipelineRequestProto(pipeline); - expect(proto).not.to.be.null; + + const expectedStructuredPipelineProto = + '{"pipeline":{"stages":[{"name":"collection","options":{},"args":[{"referenceValue":"/customers"}]},{"name":"where","options":{},"args":[{"functionValue":{"name":"equal","args":[{"fieldReferenceValue":"country"},{"stringValue":"United Kingdom"}]}}]}]}}'; + expect(JSON.stringify(proto.structuredPipeline)).to.equal( + expectedStructuredPipelineProto + ); + }); + + it('supports PipelineSnapshot serialization to proto', async () => { + // Perform the same test as the console + const pipeline = firestore + .pipeline() + .collection(randomCol) + .sort(field('title').ascending()) + .limit(1); + + const result = await execute(pipeline); + + expect(result.results[0]._fieldsProto()).to.deep.equal({ + 'author': { + 'stringValue': 'George Orwell' + }, + 'awards': { + 'mapValue': { + 'fields': { + 'prometheus': { + 'booleanValue': true + } + } + } + }, + 'embedding': { + 'mapValue': { + 'fields': { + '__type__': { + 'stringValue': '__vector__' + }, + 'value': { + 'arrayValue': { + 'values': [ + { + 'doubleValue': 1 + }, + { + 'doubleValue': 1 + }, + { + 'doubleValue': 1 + }, + { + 'doubleValue': 1 + }, + { + 'doubleValue': 1 + }, + { + 'doubleValue': 1 + }, + { + 'doubleValue': 1 + }, + { + 'doubleValue': 10 + }, + { + 'doubleValue': 1 + }, + { + 'doubleValue': 1 + } + ] + } + } + } + } + }, + 'genre': { + 'stringValue': 'Dystopian' + }, + 'published': { + 'integerValue': '1949' + }, + 'rating': { + 'doubleValue': 4.2 + }, + 'tags': { + 'arrayValue': { + 'values': [ + { + 'stringValue': 'surveillance' + }, + { + 'stringValue': 'totalitarianism' + }, + { + 'stringValue': 'propaganda' + } + ] + } + }, + 'title': { + 'stringValue': '1984' + } + }); + }); + + it('performs validation', async () => { + expect(() => { + const pipeline = firestore + .pipeline() + .collection('customers') + .where(field('country').equal(new Map([]))); + + _internalPipelineToExecutePipelineRequestProto(pipeline); + }).to.throw(); }); }); @@ -878,6 +984,27 @@ const timestampDeltaMS = 1000; }); }); + it('throws on Duplicate aliases', async () => { + expect(() => + firestore + .pipeline() + .collection(randomCol.path) + .aggregate(countAll().as('count'), count('foo').as('count')) + ).to.throw("Duplicate alias or field 'count'"); + }); + + it('throws on duplicate group aliases', async () => { + expect(() => + firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [countAll().as('count')], + groups: ['bax', field('bar').as('bax')] + }) + ).to.throw("Duplicate alias or field 'bax'"); + }); + it('supports aggregate options', async () => { let snapshot = await execute( firestore @@ -1080,6 +1207,16 @@ const timestampDeltaMS = 1000; ); }); + it('throws on Duplicate aliases', async () => { + expect(() => { + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(constant(1).as('foo'), constant(2).as('foo')); + }).to.throw("Duplicate alias or field 'foo'"); + }); + it('supports options', async () => { const snapshot = await execute( firestore @@ -1153,6 +1290,17 @@ const timestampDeltaMS = 1000; ); }); + it('throws on Duplicate aliases', async () => { + expect(() => + firestore + .pipeline() + .collection(randomCol.path) + .select('title', 'author') + .addFields(constant('bar').as('foo'), constant('baz').as('foo')) + .sort(field('author').ascending()) + ).to.throw("Duplicate alias or field 'foo'"); + }); + it('supports options', async () => { const snapshot = await execute( firestore @@ -1217,7 +1365,6 @@ const timestampDeltaMS = 1000; .select('title', 'author') .sort(field('author').ascending()) .removeFields(field('author')) - .sort(field('author').ascending()) ); expectResults( snapshot, @@ -1258,7 +1405,6 @@ const timestampDeltaMS = 1000; .removeFields({ fields: [field('author'), 'genre'] }) - .sort(field('author').ascending()) ); expectResults( snapshot, @@ -1299,7 +1445,6 @@ const timestampDeltaMS = 1000; .select('title', 'author') .sort(field('author').ascending()) .removeFields(field('author')) - .sort(field('author').ascending()) ); expectResults( snapshot, @@ -1340,7 +1485,6 @@ const timestampDeltaMS = 1000; .removeFields({ fields: [field('author'), 'genre'] }) - .sort(field('author').ascending()) ); expectResults( snapshot, @@ -2585,8 +2729,8 @@ const timestampDeltaMS = 1000; .sort(field('rating').descending()) .limit(1) .select( - isNull('rating').as('ratingIsNull'), - isNan('rating').as('ratingIsNaN'), + equal('rating', null).as('ratingIsNull'), + equal('rating', NaN).as('ratingIsNaN'), isError(divide(constant(1), constant(0))).as('isError'), ifError(divide(constant(1), constant(0)), constant('was error')).as( 'ifError' @@ -2598,8 +2742,8 @@ const timestampDeltaMS = 1000; .not() .as('ifErrorBooleanExpression'), isAbsent('foo').as('isAbsent'), - isNotNull('title').as('titleIsNotNull'), - isNotNan('cost').as('costIsNotNan'), + notEqual('title', null).as('titleIsNotNull'), + notEqual('cost', NaN).as('costIsNotNan'), exists('fooBarBaz').as('fooBarBazExists'), field('title').exists().as('titleExists') ) @@ -2624,8 +2768,8 @@ const timestampDeltaMS = 1000; .sort(field('rating').descending()) .limit(1) .select( - field('rating').isNull().as('ratingIsNull'), - field('rating').isNan().as('ratingIsNaN'), + field('rating').equal(null).as('ratingIsNull'), + field('rating').equal(NaN).as('ratingIsNaN'), divide(constant(1), constant(0)).isError().as('isError'), divide(constant(1), constant(0)) .ifError(constant('was error')) @@ -2636,8 +2780,8 @@ const timestampDeltaMS = 1000; .not() .as('ifErrorBooleanExpression'), field('foo').isAbsent().as('isAbsent'), - field('title').isNotNull().as('titleIsNotNull'), - field('cost').isNotNan().as('costIsNotNan') + field('title').notEqual(null).as('titleIsNotNull'), + field('cost').notEqual(NaN).as('costIsNotNan') ) ); expectResults(snapshot, { @@ -3779,15 +3923,30 @@ const timestampDeltaMS = 1000; firestore .pipeline() .collection(randomCol.path) - .addFields( - constant(" The Hitchhiker's Guide to the Galaxy ").as('spacedTitle') + .replaceWith( + map({ + spacedTitle: " The Hitchhiker's Guide to the Galaxy ", + userNameWithQuotes: '"alice"', + bytes: Bytes.fromUint8Array( + Uint8Array.from([0x00, 0x01, 0x02, 0x00, 0x00]) + ) + }) + ) + .select( + trim('spacedTitle').as('trimmedTitle'), + field('spacedTitle'), + field('userNameWithQuotes').trim('"').as('userName'), + field('bytes') + .trim(Bytes.fromUint8Array(Uint8Array.from([0x00]))) + .as('bytes') ) - .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" + trimmedTitle: "The Hitchhiker's Guide to the Galaxy", + userName: 'alice', + bytes: Bytes.fromUint8Array(Uint8Array.from([0x01, 0x02])) }); }); @@ -3891,22 +4050,6 @@ const timestampDeltaMS = 1000; ).lessThan(5000); }); - // Not implemented in backend - // eslint-disable-next-line no-restricted-properties - it.skip('supports error', async () => { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol.path) - .limit(1) - .select(isError(error('test error')).as('error')) - ); - - expectResults(snapshot, { - 'error': true - }); - }); - it('supports ifAbsent', async () => { const snapshot = await execute( firestore @@ -3983,6 +4126,168 @@ const timestampDeltaMS = 1000; }); }); + it('truncate timestamp', async () => { + const results = await execute( + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .replaceWith( + map({ + timestamp: new Timestamp( + Date.UTC(2025, 10, 30, 1, 2, 3) / 1000, + 456789 + ) + }) + ) + .select( + timestampTruncate('timestamp', 'year').as('trunc_year'), + timestampTruncate(field('timestamp'), 'month').as('trunc_month'), + timestampTruncate(field('timestamp'), constant('day')).as( + 'trunc_day' + ), + field('timestamp') + .timestampTruncate(constant('day'), 'MST') + .as('trunc_day_mst'), + field('timestamp').timestampTruncate('hour').as('trunc_hour'), + field('timestamp') + .timestampTruncate(constant('minute')) + .as('trunc_minute'), + field('timestamp').timestampTruncate('second').as('trunc_second') + ) + ); + + expectResults(results, { + 'trunc_year': new Timestamp(Date.UTC(2025, 0) / 1000, 0), + 'trunc_month': new Timestamp(Date.UTC(2025, 10) / 1000, 0), + 'trunc_day': new Timestamp(Date.UTC(2025, 10, 30) / 1000, 0), + 'trunc_day_mst': new Timestamp( + Date.UTC(2025, 10, 29) / 1000 + 7 * 3600, + 0 + ), + 'trunc_hour': new Timestamp(Date.UTC(2025, 10, 30, 1) / 1000, 0), + 'trunc_minute': new Timestamp(Date.UTC(2025, 10, 30, 1, 2) / 1000, 0), + 'trunc_second': new Timestamp(Date.UTC(2025, 10, 30, 1, 2, 3) / 1000, 0) + }); + }); + + it('supports split', async () => { + const results = await execute( + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .replaceWith( + map({ + csv: 'foo,bar,baz', + data: 'baz:bar:foo', + csvDelimeter: ',', + bytes: Bytes.fromUint8Array( + Uint8Array.from([0x01, 0x00, 0x02, 0x00, 0x03]) + ) + }) + ) + .select( + split('csv', field('csvDelimeter')).as('csv'), + split(field('data'), ':').as('data'), + field('bytes') + .split(constant(Bytes.fromUint8Array(Uint8Array.from([0x00])))) + .as('bytes') + ) + ); + + expectResults(results, { + csv: ['foo', 'bar', 'baz'], + data: ['baz', 'bar', 'foo'], + bytes: [ + Bytes.fromUint8Array(Uint8Array.from([0x01])), + Bytes.fromUint8Array(Uint8Array.from([0x02])), + Bytes.fromUint8Array(Uint8Array.from([0x03])) + ] + }); + + void expect( + execute( + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .replaceWith( + map({ + csv: 'foo,bar,baz' + }) + ) + .select( + field('csv') + .split(constant(Bytes.fromUint8Array(Uint8Array.from([0x00])))) + .as('dontSplitStringAndBytes') + ) + ) + ).to.be.rejected; + }); + + it('supports type', async () => { + const result = await execute( + firestore + .pipeline() + .collection(randomCol) + .limit(1) + .replaceWith( + map({ + int: constant(1), + float: constant(1.1), + str: constant('a string'), + bool: constant(true), + null: constant(null), + geoPoint: constant(new GeoPoint(0.1, 0.2)), + timestamp: constant(new Timestamp(123456, 0)), + date: constant(new Date()), + bytes: constant( + Bytes.fromUint8Array(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 0])) + ), + docRef: constant(doc(firestore, 'foo', 'bar')), + vector: constant(vector([1, 2, 3])), + map: map({ + 'number': 1, + 'string': 'a string' + }), + array: array([1, 'a string']) + }) + ) + .select( + type('int').as('int'), + field('float').type().as('float'), + field('str').type().as('str'), + type('bool').as('bool'), + type('null').as('null'), + type('geoPoint').as('geoPoint'), + type('timestamp').as('timestamp'), + type('date').as('date'), + type('bytes').as('bytes'), + type('docRef').as('docRef'), + type('vector').as('vector'), + type('map').as('map'), + type('array').as('array') + ) + ); + + expectResults(result, { + int: 'int64', + float: 'float64', + str: 'string', + bool: 'boolean', + null: 'null', + geoPoint: 'geo_point', + timestamp: 'timestamp', + date: 'timestamp', + bytes: 'bytes', + docRef: 'reference', + vector: 'vector', + map: 'map', + array: 'array' + }); + }); + // TODO(new-expression): Add new expression tests above this line }); diff --git a/packages/firestore/test/integration/api/provider.test.ts b/packages/firestore/test/integration/api/provider.test.ts index cc7888a5385..95d825c2a9e 100644 --- a/packages/firestore/test/integration/api/provider.test.ts +++ b/packages/firestore/test/integration/api/provider.test.ts @@ -29,7 +29,8 @@ import { enableIndexedDbPersistence, setDoc, memoryLocalCache, - getDocFromCache + getDocFromCache, + ensureFirestoreConfigured } from '../util/firebase_export'; import { DEFAULT_SETTINGS } from '../util/settings'; @@ -200,4 +201,17 @@ describe('Firestore Provider', () => { return terminate(firestore).then(() => terminate(firestore)); }); + + it('passes API key to database info', () => { + const app = initializeApp( + { apiKey: 'fake-api-key-x', projectId: 'test-project' }, + 'test-app-getFirestore-x' + ); + const fs = getFirestore(app); + ensureFirestoreConfigured(fs); + + expect(fs._firestoreClient?._databaseInfo.apiKey).to.equal( + 'fake-api-key-x' + ); + }); }); diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index a12c843bf26..af3a5330edf 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -19,6 +19,7 @@ import { isNode } from '@firebase/util'; import { expect } from 'chai'; import { addEqualityMatcher } from '../../util/equality_matcher'; +import { it } from '../../util/mocha_extensions'; import { Deferred } from '../../util/promise'; import { EventsAccumulator } from '../util/events_accumulator'; import { @@ -711,7 +712,7 @@ apiDescribe('Queries', persistence => { }); // eslint-disable-next-line no-restricted-properties - (USE_EMULATOR ? it.skip : it)( + it.skipEmulator.skipEnterprise( 'can catch error message for missing index with error handler', () => { return withEmptyTestCollection(persistence, async coll => { @@ -986,7 +987,7 @@ apiDescribe('Queries', persistence => { }); }); - it('can use IN filters', async () => { + it.skipEnterprise('can use IN filters', async () => { const testDocs = { a: { zip: 98101 }, b: { zip: 91102 }, @@ -1145,7 +1146,7 @@ apiDescribe('Queries', persistence => { }); }); - it('can use array-contains-any filters', async () => { + it.skipEnterprise('can use array-contains-any filters', async () => { const testDocs = { a: { array: [42] }, b: { array: ['a', 42, 'c'] }, diff --git a/packages/firestore/test/integration/api/query_to_pipeline.test.ts b/packages/firestore/test/integration/api/query_to_pipeline.test.ts index 3cb3b765d41..ab09a83d739 100644 --- a/packages/firestore/test/integration/api/query_to_pipeline.test.ts +++ b/packages/firestore/test/integration/api/query_to_pipeline.test.ts @@ -40,13 +40,13 @@ import { documentId, addDoc, getDoc, - or + or, + getDocs } from '../util/firebase_export'; import { apiDescribe, PERSISTENCE_MODE_UNSPECIFIED, - withTestCollection, - itIf + withTestCollection } from '../util/helpers'; import { execute, PipelineSnapshot } from '../util/pipeline_export'; @@ -54,13 +54,9 @@ use(chaiAsPromised); setLogLevel('debug'); -const testUnsupportedFeatures: boolean | 'only' = false; - // This is the Query integration tests from the lite API (no cache support) // with some additional test cases added for more complete coverage. -(process.env.FIRESTORE_TARGET_DB_ID === 'enterprise' - ? apiDescribe.only - : apiDescribe.skip)('Query to Pipeline', persistence => { +apiDescribe.skipClassic('Query to Pipeline', persistence => { addEqualityMatcher(); function verifyResults( @@ -480,8 +476,7 @@ const testUnsupportedFeatures: boolean | 'only' = false; ); }); - // needs subcollection support - itIf(testUnsupportedFeatures)('supports collection groups', () => { + it('supports collection groups', () => { return withTestCollection( PERSISTENCE_MODE_UNSPECIFIED, {}, @@ -507,34 +502,30 @@ const testUnsupportedFeatures: boolean | 'only' = false; ); }); - // needs subcollection support - itIf(testUnsupportedFeatures)( - 'supports query over collection path with special characters', - () => { - return withTestCollection( - PERSISTENCE_MODE_UNSPECIFIED, - {}, - async (collRef, db) => { - const docWithSpecials = doc(collRef, 'so!@#$%^&*()_+special'); + it('supports query over collection path with special characters', () => { + return withTestCollection( + PERSISTENCE_MODE_UNSPECIFIED, + {}, + async (collRef, db) => { + const docWithSpecials = doc(collRef, 'so!@#$%^&*()_+special'); - const collectionWithSpecials = collection( - docWithSpecials, - 'so!@#$%^&*()_+special' - ); - await addDoc(collectionWithSpecials, { foo: 1 }); - await addDoc(collectionWithSpecials, { foo: 2 }); + const collectionWithSpecials = collection( + docWithSpecials, + 'so!@#$%^&*()_+special' + ); + await addDoc(collectionWithSpecials, { foo: 1 }); + await addDoc(collectionWithSpecials, { foo: 2 }); - const snapshot = await execute( - db - .pipeline() - .createFrom(query(collectionWithSpecials, orderBy('foo', 'asc'))) - ); + const snapshot = await execute( + db + .pipeline() + .createFrom(query(collectionWithSpecials, orderBy('foo', 'asc'))) + ); - verifyResults(snapshot, { foo: 1 }, { foo: 2 }); - } - ); - } - ); + verifyResults(snapshot, { foo: 1 }, { foo: 2 }); + } + ); + }); it('supports multiple inequality on same field', () => { return withTestCollection( @@ -647,7 +638,7 @@ const testUnsupportedFeatures: boolean | 'only' = false; async (collRef, db) => { const query1 = query(collRef, where('bar', '!=', NaN)); const snapshot = await execute(db.pipeline().createFrom(query1)); - verifyResults(snapshot, { foo: 2, bar: 1 }); + verifyResults(snapshot, { foo: 2, bar: 1 }, { foo: 3, bar: 'bar' }); } ); }); @@ -661,8 +652,10 @@ const testUnsupportedFeatures: boolean | 'only' = false; }, async (collRef, db) => { const query1 = query(collRef, where('bar', '==', null)); + const classicSnapshot = await getDocs(query1); + const classicData = classicSnapshot.docs.map(d => d.data()); const snapshot = await execute(db.pipeline().createFrom(query1)); - verifyResults(snapshot, { foo: 1, bar: null }); + verifyResults(snapshot, ...classicData); } ); }); @@ -676,8 +669,10 @@ const testUnsupportedFeatures: boolean | 'only' = false; }, async (collRef, db) => { const query1 = query(collRef, where('bar', '!=', null)); + const classicSnapshot = await getDocs(query1); + const classicData = classicSnapshot.docs.map(d => d.data()); const snapshot = await execute(db.pipeline().createFrom(query1)); - verifyResults(snapshot, { foo: 2, bar: 1 }); + verifyResults(snapshot, ...classicData); } ); }); @@ -767,7 +762,7 @@ const testUnsupportedFeatures: boolean | 'only' = false; ); }); - itIf(testUnsupportedFeatures)('supports not in', () => { + it('supports not in', () => { return withTestCollection( PERSISTENCE_MODE_UNSPECIFIED, { @@ -776,7 +771,11 @@ const testUnsupportedFeatures: boolean | 'only' = false; 3: { foo: 3, bar: 10 } }, async (collRef, db) => { - const query1 = query(collRef, where('bar', 'not-in', [0, 10, 20])); + const query1 = query( + collRef, + where('bar', 'not-in', [0, 10, 20]), + orderBy('foo') + ); const snapshot = await execute(db.pipeline().createFrom(query1)); verifyResults(snapshot, { foo: 1, bar: 2 }, { foo: 2, bar: 1 }); } diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 1ea11fcc30f..e1afc39932f 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -18,6 +18,8 @@ import { isIndexedDBAvailable } from '@firebase/util'; import { expect } from 'chai'; +import { describe } from '../../util/mocha_extensions'; + import { clearIndexedDbPersistence, collection, @@ -177,7 +179,10 @@ export function isPersistenceAvailable(): boolean { * persistence both disabled and enabled (if the browser is supported). */ function apiDescribeInternal( - describeFn: Mocha.PendingSuiteFunction, + describeFn: + | Mocha.PendingSuiteFunction + | Mocha.SuiteFunction + | Mocha.ExclusiveSuiteFunction, message: string, testSuite: (persistence: PersistenceMode) => void ): void { @@ -204,6 +209,9 @@ interface ApiDescribe { (message: string, testSuite: (persistence: PersistenceMode) => void): void; skip: ApiSuiteFunction; only: ApiSuiteFunction; + skipEnterprise: ApiSuiteFunction; + skipEmulator: ApiSuiteFunction; + skipClassic: ApiSuiteFunction; } export const apiDescribe = apiDescribeInternal.bind( @@ -214,6 +222,15 @@ export const apiDescribe = apiDescribeInternal.bind( apiDescribe.skip = apiDescribeInternal.bind(null, describe.skip); // eslint-disable-next-line no-restricted-properties apiDescribe.only = apiDescribeInternal.bind(null, describe.only); +apiDescribe.skipEnterprise = apiDescribeInternal.bind( + null, + describe.skipEnterprise +); +apiDescribe.skipClassic = apiDescribeInternal.bind(null, describe.skipClassic); +apiDescribe.skipEmulator = apiDescribeInternal.bind( + null, + describe.skipEmulator +); /** Converts the documents in a QuerySnapshot to an array with the data of each document. */ export function toDataArray(docSet: QuerySnapshot): DocumentData[] { @@ -580,10 +597,3 @@ export async function checkOnlineAndOfflineResultsMatch( expect(expectedDocs).to.deep.equal(toIds(docsFromServer)); } } - -export function itIf( - condition: boolean | 'only' -): Mocha.TestFunction | Mocha.PendingTestFunction { - // eslint-disable-next-line no-restricted-properties - return condition === 'only' ? it.only : condition ? it : it.skip; -} diff --git a/packages/firestore/test/integration/util/internal_helpers.ts b/packages/firestore/test/integration/util/internal_helpers.ts index e5e64b5fbf4..b196599f408 100644 --- a/packages/firestore/test/integration/util/internal_helpers.ts +++ b/packages/firestore/test/integration/util/internal_helpers.ts @@ -62,7 +62,8 @@ export function getDefaultDatabaseInfo(): DatabaseInfo { DEFAULT_SETTINGS.experimentalLongPollingOptions ?? {} ), /*use FetchStreams= */ false, - /*isUsingEmulator=*/ false + /*isUsingEmulator=*/ false, + undefined ); } diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index 7fb7eafcb1e..de484a22fe5 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -93,6 +93,7 @@ import { DEFAULT_SETTINGS, USE_EMULATOR } from '../integration/util/settings'; +import { it, describe } from '../util/mocha_extensions'; import { Post, @@ -2457,27 +2458,24 @@ describe('Count queries', () => { // production, since the Firestore Emulator does not require index creation // and will, therefore, never fail in this situation. // eslint-disable-next-line no-restricted-properties - (USE_EMULATOR ? it.skip : it)( - 'getCount error message contains console link if missing index', - () => { - return withTestCollection(async coll => { - const query_ = query( - coll, - where('key1', '==', 42), - where('key2', '<', 42) + it.skip('getCount error message contains console link if missing index', () => { + return withTestCollection(async coll => { + const query_ = query( + coll, + where('key1', '==', 42), + where('key2', '<', 42) + ); + // TODO(b/316359394) Remove the special logic for non-default databases + // once cl/582465034 is rolled out to production. + if (coll.firestore._databaseId.isDefaultDatabase) { + await expect(getCount(query_)).to.be.eventually.rejectedWith( + /index.*https:\/\/console\.firebase\.google\.com/ ); - // TODO(b/316359394) Remove the special logic for non-default databases - // once cl/582465034 is rolled out to production. - if (coll.firestore._databaseId.isDefaultDatabase) { - await expect(getCount(query_)).to.be.eventually.rejectedWith( - /index.*https:\/\/console\.firebase\.google\.com/ - ); - } else { - await expect(getCount(query_)).to.be.eventually.rejected; - } - }); - } - ); + } else { + await expect(getCount(query_)).to.be.eventually.rejected; + } + }); + }); }); describe('Aggregate queries', () => { @@ -2766,7 +2764,7 @@ describe('Aggregate queries', () => { // production, since the Firestore Emulator does not require index creation // and will, therefore, never fail in this situation. // eslint-disable-next-line no-restricted-properties - (USE_EMULATOR ? it.skip : it)( + it.skipEmulator.skipEnterprise( 'getAggregate error message contains console link if missing index', () => { return withTestCollection(async coll => { @@ -2900,26 +2898,29 @@ describe('Aggregate queries - sum / average', () => { }); }); - it('fails when exceeding the max (5) aggregations using getAggregationFromServer', () => { - const testDocs = [ - { author: 'authorA', title: 'titleA', pages: 100 }, - { author: 'authorB', title: 'titleB', pages: 50 } - ]; - return withTestCollectionAndInitialData(testDocs, async coll => { - const promise = getAggregate(coll, { - totalPages: sum('pages'), - averagePages: average('pages'), - count: count(), - totalPagesX: sum('pages'), - averagePagesY: average('pages'), - countZ: count() - }); + it.skipEmulator.skipEnterprise( + 'fails when exceeding the max (5) aggregations using getAggregationFromServer', + () => { + const testDocs = [ + { author: 'authorA', title: 'titleA', pages: 100 }, + { author: 'authorB', title: 'titleB', pages: 50 } + ]; + return withTestCollectionAndInitialData(testDocs, async coll => { + const promise = getAggregate(coll, { + totalPages: sum('pages'), + averagePages: average('pages'), + count: count(), + totalPagesX: sum('pages'), + averagePagesY: average('pages'), + countZ: count() + }); - await expect(promise).to.eventually.be.rejectedWith( - /maximum number of aggregations/ - ); - }); - }); + await expect(promise).to.eventually.be.rejectedWith( + /maximum number of aggregations/ + ); + }); + } + ); // Only run tests that require indexes against the emulator, because we don't // have a way to dynamically create the indexes when running the tests. diff --git a/packages/firestore/test/lite/pipeline.test.ts b/packages/firestore/test/lite/pipeline.test.ts index cc549dcda7e..6d7d1dee375 100644 --- a/packages/firestore/test/lite/pipeline.test.ts +++ b/packages/firestore/test/lite/pipeline.test.ts @@ -16,6 +16,7 @@ */ // eslint-disable-next-line import/no-extraneous-dependencies +import { FirebaseError } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; @@ -25,6 +26,30 @@ import { getFirestore, terminate } from '../../src/lite-api/database'; +import { documentId as documentIdFieldPath } from '../../src/lite-api/field_path'; +import { vector } from '../../src/lite-api/field_value_impl'; +import { GeoPoint } from '../../src/lite-api/geo_point'; +import { + pipelineResultEqual, + PipelineSnapshot +} from '../../src/lite-api/pipeline-result'; +import { execute } from '../../src/lite-api/pipeline_impl'; +import { + DocumentData, + CollectionReference, + collection, + doc, + DocumentReference +} from '../../src/lite-api/reference'; +import { addDoc, setDoc, deleteDoc } from '../../src/lite-api/reference_impl'; +import { FindNearestStageOptions } from '../../src/lite-api/stage_options'; +import { Timestamp } from '../../src/lite-api/timestamp'; +import { writeBatch } from '../../src/lite-api/write_batch'; +import { addEqualityMatcher } from '../util/equality_matcher'; +import { describe } from '../util/mocha_extensions'; +import { Deferred } from '../util/promise'; + +import { withTestCollection } from './helpers'; import { field, and, @@ -42,10 +67,8 @@ import { isAbsent, isError, or, - isNotNan, map, - isNotNull, - isNull, + length, mod, documentId, equal, @@ -54,7 +77,6 @@ import { countIf, lessThanOrEqual, greaterThan, - arrayConcat, arrayContains, arrayContainsAny, equalAny, @@ -64,7 +86,6 @@ import { logicalMaximum, logicalMinimum, exists, - isNan, reverse, like, regexContains, @@ -93,51 +114,43 @@ import { FunctionExpression, BooleanExpression, AggregateFunction, - sum, stringConcat, arrayContainsAll, arrayLength, charLength, divide, - abs, not, toLower, toUpper, trim, + byteLength, arrayGet, - byteLength -} from '../../src/lite-api/expressions'; -import { documentId as documentIdFieldPath } from '../../src/lite-api/field_path'; -import { vector } from '../../src/lite-api/field_value_impl'; -import { GeoPoint } from '../../src/lite-api/geo_point'; -import { - pipelineResultEqual, - PipelineSnapshot -} from '../../src/lite-api/pipeline-result'; -import { execute } from '../../src/lite-api/pipeline_impl'; -import { - DocumentData, - CollectionReference, - collection, - doc -} from '../../src/lite-api/reference'; -import { addDoc, setDoc } from '../../src/lite-api/reference_impl'; -import { FindNearestStageOptions } from '../../src/lite-api/stage_options'; -import { Timestamp } from '../../src/lite-api/timestamp'; -import { writeBatch } from '../../src/lite-api/write_batch'; -import { itIf } from '../integration/util/helpers'; -import { addEqualityMatcher } from '../util/equality_matcher'; -import { Deferred } from '../util/promise'; - -import { withTestCollection } from './helpers'; + abs, + sum, + countDistinct, + ceil, + floor, + exp, + pow, + round, + collectionId, + ln, + log, + sqrt, + stringReverse, + log10, + concat, + currentTimestamp, + ifAbsent, + join, + arraySum +} from './pipeline_export'; use(chaiAsPromised); -const testUnsupportedFeatures = false; const timestampDeltaMS = 1000; -// eslint-disable-next-line no-restricted-properties -describe.skip('Firestore Pipelines', () => { +describe.skipClassic('Firestore Pipelines', () => { addEqualityMatcher(); let firestore: Firestore; @@ -329,8 +342,7 @@ describe.skip('Firestore Pipelines', () => { expect(snapshot.results.length).to.equal(0); }); - // Skipping because __name__ is not currently working in DBE - itIf(testUnsupportedFeatures)('full snapshot as expected', async () => { + it('full snapshot as expected', async () => { const ppl = firestore .pipeline() .collection(randomCol.path) @@ -512,31 +524,26 @@ describe.skip('Firestore Pipelines', () => { 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, @@ -555,6 +562,17 @@ describe.skip('Firestore Pipelines', () => { ); expectResults(snapshot, doc1.id, doc2.id); }); + + it('can create pipeline from a query', async () => { + const snapshot = await execute( + firestore + .pipeline() + .createFrom(randomCol) + .sort(field('__name__').ascending()) + .limit(1) + ); + expectResults(snapshot, 'book1'); + }); }); describe('supported data types', () => { @@ -630,7 +648,6 @@ describe.skip('Firestore Pipelines', () => { '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', @@ -788,6 +805,30 @@ describe.skip('Firestore Pipelines', () => { } }); }); + + 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', () => { @@ -821,6 +862,37 @@ describe.skip('Firestore Pipelines', () => { }); }); + it('supports aggregate options', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .aggregate({ + accumulators: [countAll().as('count')] + }) + ); + expectResults(snapshot, { count: 10 }); + + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('genre', 'Science Fiction')) + .aggregate( + countAll().as('count'), + average('rating').as('avgRating'), + maximum('rating').as('maxRating'), + sum('rating').as('sumRating') + ) + ); + expectResults(snapshot, { + count: 2, + avgRating: 4.4, + maxRating: 4.6, + sumRating: 8.8 + }); + }); + it('rejects groups without accumulators', async () => { await expect( execute( @@ -897,6 +969,16 @@ describe.skip('Firestore Pipelines', () => { ); 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', () => { @@ -922,6 +1004,34 @@ describe.skip('Firestore Pipelines', () => { { genre: 'Southern Gothic', author: 'Harper Lee' } ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .distinct('genre', 'author') + .sort({ + orderings: [ + field('genre').ascending(), + field('author').ascending() + ] + }) + ); + expectResults( + snapshot, + { genre: 'Dystopian', author: 'George Orwell' }, + { genre: 'Dystopian', author: 'Margaret Atwood' }, + { genre: 'Fantasy', author: 'J.R.R. Tolkien' }, + { genre: 'Magical Realism', author: 'Gabriel García Márquez' }, + { genre: 'Modernist', author: 'F. Scott Fitzgerald' }, + { genre: 'Psychological Thriller', author: 'Fyodor Dostoevsky' }, + { genre: 'Romance', author: 'Jane Austen' }, + { genre: 'Science Fiction', author: 'Douglas Adams' }, + { genre: 'Science Fiction', author: 'Frank Herbert' }, + { genre: 'Southern Gothic', author: 'Harper Lee' } + ); + }); }); describe('select stage', () => { @@ -953,6 +1063,25 @@ describe.skip('Firestore Pipelines', () => { { 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', () => { @@ -1007,6 +1136,60 @@ describe.skip('Firestore Pipelines', () => { } ); }); + + 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', () => { @@ -1018,7 +1201,6 @@ describe.skip('Firestore Pipelines', () => { .select('title', 'author') .sort(field('author').ascending()) .removeFields(field('author')) - .sort(field('author').ascending()) ); expectResults( snapshot, @@ -1048,38 +1230,160 @@ describe.skip('Firestore Pipelines', () => { } ); }); - }); - describe('where stage', () => { - it('where with and (2 conditions)', async () => { + it('supports options', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where( - and( - greaterThan('rating', 4.5), - equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) - ) - ) + .select('title', 'author', 'genre') + .sort(field('author').ascending()) + .removeFields({ + fields: [field('author'), 'genre'] + }) + ); + 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" + } ); - expectResults(snapshot, 'book10', 'book4'); }); - it('where with and (3 conditions)', async () => { + }); + + describe('findNearest stage', () => { + it('can find nearest', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .where( - and( - greaterThan('rating', 4.5), - equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), - lessThan('published', 1965) - ) - ) - ); + .select('title', 'author') + .sort(field('author').ascending()) + .removeFields(field('author')) + ); + 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'] + }) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'The Great Gatsby' + }, + { title: 'Dune' }, + { + title: 'Crime and Punishment' + }, + { + title: 'One Hundred Years of Solitude' + }, + { title: '1984' }, + { + title: 'To Kill a Mockingbird' + }, + { + title: 'The Lord of the Rings' + }, + { title: 'Pride and Prejudice' }, + { + title: "The Handmaid's Tale" + } + ); + }); + }); + + describe('where stage', () => { + it('where with and (2 conditions)', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ) + ) + ); + expectResults(snapshot, 'book10', 'book4'); + }); + + it('where with and (3 conditions)', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where( + and( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']), + lessThan('published', 1965) + ) + ) + ); expectResults(snapshot, 'book4'); }); + it('where with or', async () => { const snapshot = await execute( firestore @@ -1126,6 +1430,21 @@ describe.skip('Firestore Pipelines', () => { { title: "The Handmaid's Tale" } ); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where({ + condition: and( + greaterThan('rating', 4.5), + equalAny('genre', ['Science Fiction', 'Romance', 'Fantasy']) + ) + }) + ); + expectResults(snapshot, 'book10', 'book4'); + }); }); describe('sort, offset, and limit stages', () => { @@ -1146,6 +1465,26 @@ describe.skip('Firestore Pipelines', () => { { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } ); }); + + it('sort, offset, and limit stages support options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort({ + orderings: [field('author').ascending()] + }) + .offset({ offset: 5 }) + .limit({ limit: 3 }) + .select('title', 'author') + ); + expectResults( + snapshot, + { title: '1984', author: 'George Orwell' }, + { title: 'To Kill a Mockingbird', author: 'Harper Lee' }, + { title: 'The Lord of the Rings', author: 'J.R.R. Tolkien' } + ); + }); }); describe('raw stage', () => { @@ -1158,7 +1497,7 @@ describe.skip('Firestore Pipelines', () => { { title: field('title'), metadata: { - 'author': field('author') + author: field('author') } } ]) @@ -1279,6 +1618,38 @@ describe.skip('Firestore Pipelines', () => { } ); }); + + 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', () => { @@ -1317,6 +1688,21 @@ describe.skip('Firestore Pipelines', () => { baz: { title: "The Hitchhiker's Guide to the Galaxy" } }); }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .replaceWith({ map: 'awards' }) + ); + expectResults(snapshot, { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }); + }); }); describe('sample stage', () => { @@ -1339,7 +1725,7 @@ describe.skip('Firestore Pipelines', () => { 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 @@ -1356,8 +1742,7 @@ describe.skip('Firestore Pipelines', () => { }); describe('union stage', () => { - // __name__ not currently supported by dbe - itIf(testUnsupportedFeatures)('run pipeline with union', async () => { + it('run pipeline with union', async () => { const snapshot = await execute( firestore .pipeline() @@ -1389,6 +1774,39 @@ describe.skip('Firestore Pipelines', () => { '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', () => { @@ -1460,13 +1878,14 @@ describe.skip('Firestore Pipelines', () => { } ); }); - it('unnest an expr', async () => { + + it('unnest with index field', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) - .unnest(array([1, 2, 3]).as('copy')) + .unnest(field('tags').as('tag'), 'tagsIndex') .select( 'title', 'author', @@ -1474,9 +1893,10 @@ describe.skip('Firestore Pipelines', () => { 'published', 'rating', 'tags', - 'copy', + 'tag', 'awards', - 'nestedField' + 'nestedField', + 'tagsIndex' ) ); expectResults( @@ -1488,13 +1908,14 @@ describe.skip('Firestore Pipelines', () => { published: 1979, rating: 4.2, tags: ['comedy', 'space', 'adventure'], - copy: 1, + tag: 'comedy', awards: { hugo: true, nebula: false, others: { unknown: { year: 1980 } } }, - nestedField: { 'level.1': { 'level.2': true } } + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 0 }, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1503,13 +1924,14 @@ describe.skip('Firestore Pipelines', () => { published: 1979, rating: 4.2, tags: ['comedy', 'space', 'adventure'], - copy: 2, + tag: 'space', awards: { hugo: true, nebula: false, others: { unknown: { year: 1980 } } }, - nestedField: { 'level.1': { 'level.2': true } } + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 1 }, { title: "The Hitchhiker's Guide to the Galaxy", @@ -1518,67 +1940,213 @@ describe.skip('Firestore Pipelines', () => { published: 1979, rating: 4.2, tags: ['comedy', 'space', 'adventure'], - copy: 3, + tag: 'adventure', awards: { hugo: true, nebula: false, others: { unknown: { year: 1980 } } }, - nestedField: { 'level.1': { 'level.2': true } } + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 2 } ); }); - }); - - describe('findNearest stage', () => { - it('run pipeline with findNearest', async () => { - const measures: Array = [ - 'euclidean', - 'dot_product', - 'cosine' - ]; - for (const measure of measures) { - const snapshot = await execute( - firestore - .pipeline() - .collection(randomCol) - .findNearest({ - field: 'embedding', - vectorValue: vector([10, 1, 3, 1, 2, 1, 1, 1, 1, 1]), - limit: 3, - distanceMeasure: measure - }) - .select('title') - ); - expectResults( - snapshot, - { - title: "The Hitchhiker's Guide to the Galaxy" - }, - { - title: 'One Hundred Years of Solitude' - }, - { - title: "The Handmaid's Tale" - } - ); - } - }); - it('optionally returns the computed distance', async () => { + it('unnest an expr', async () => { const snapshot = await execute( firestore .pipeline() - .collection(randomCol) - .findNearest({ - field: 'embedding', - vectorValue: vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), - limit: 2, - distanceMeasure: 'euclidean', - distanceField: 'computedDistance' - }) - .select('title', 'computedDistance') - ); + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest(array([1, 2, 3]).as('copy')) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'copy', + 'awards', + 'nestedField' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 1, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 2, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + copy: 3, + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } } + } + ); + }); + + it('supports options', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(equal('title', "The Hitchhiker's Guide to the Galaxy")) + .unnest({ + selectable: field('tags').as('tag'), + indexField: 'tagsIndex' + }) + .select( + 'title', + 'author', + 'genre', + 'published', + 'rating', + 'tags', + 'tag', + 'awards', + 'nestedField', + 'tagsIndex' + ) + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'comedy', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 0 + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'space', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 1 + }, + { + title: "The Hitchhiker's Guide to the Galaxy", + author: 'Douglas Adams', + genre: 'Science Fiction', + published: 1979, + rating: 4.2, + tags: ['comedy', 'space', 'adventure'], + tag: 'adventure', + awards: { + hugo: true, + nebula: false, + others: { unknown: { year: 1980 } } + }, + nestedField: { 'level.1': { 'level.2': true } }, + tagsIndex: 2 + } + ); + }); + }); + + describe('findNearest stage', () => { + it('run pipeline with findNearest', async () => { + const measures: Array = [ + 'euclidean', + 'dot_product', + 'cosine' + ]; + for (const measure of measures) { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .findNearest({ + field: 'embedding', + vectorValue: vector([10, 1, 3, 1, 2, 1, 1, 1, 1, 1]), + limit: 3, + distanceMeasure: measure + }) + .select('title') + ); + expectResults( + snapshot, + { + title: "The Hitchhiker's Guide to the Galaxy" + }, + { + title: 'One Hundred Years of Solitude' + }, + { + title: "The Handmaid's Tale" + } + ); + } + }); + + it('optionally returns the computed distance', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol) + .findNearest({ + field: 'embedding', + vectorValue: vector([10, 1, 2, 1, 1, 1, 1, 1, 1, 1]), + limit: 2, + distanceMeasure: 'euclidean', + distanceField: 'computedDistance' + }) + .select('title', 'computedDistance') + ); expectResults( snapshot, { @@ -1594,8 +2162,36 @@ describe.skip('Firestore Pipelines', () => { }); }); + describe('error handling', () => { + it('error properties are propagated from the firestore backend', async () => { + try { + const myPipeline = firestore + .pipeline() + .collection(randomCol.path) + .rawStage('select', [ + // incorrect parameter type + field('title') + ]); + + await execute(myPipeline); + + expect.fail('expected pipeline.execute() to throw'); + } catch (e: unknown) { + expect(e instanceof FirebaseError).to.be.true; + const err = e as FirebaseError; + // Backend returns the code as `failed-precondition` when using the REST transport + expect(err['code']).to.equal('failed-precondition'); + expect(typeof err['message']).to.equal('string'); + + expect(err['message']).to.match( + /Request failed with error: Expected value type of MAP_VALUE when parsing 'fields' but received FIELD_REFERENCE_VALUE instead/ + ); + } + }); + }); + describe('function expressions', () => { - it('logical maximum works', async () => { + it('logical max works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1617,7 +2213,7 @@ describe.skip('Firestore Pipelines', () => { ); }); - it('logical minimum works', async () => { + it('logical min works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1639,7 +2235,7 @@ describe.skip('Firestore Pipelines', () => { ); }); - it('conditiona works', async () => { + it('conditional works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1650,20 +2246,28 @@ describe.skip('Firestore Pipelines', () => { 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' } ); }); - it('eqAny works', async () => { + it('equalAny works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1679,7 +2283,7 @@ describe.skip('Firestore Pipelines', () => { ); }); - it('notEqAny works', async () => { + it('notEqualAny works', async () => { const snapshot = await execute( firestore .pipeline() @@ -1888,8 +2492,8 @@ describe.skip('Firestore Pipelines', () => { 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) @@ -1905,20 +2509,6 @@ describe.skip('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 @@ -1978,13 +2568,21 @@ describe.skip('Firestore Pipelines', () => { .sort(field('rating').descending()) .limit(1) .select( - isNull('rating').as('ratingIsNull'), - isNan('rating').as('ratingIsNaN'), - isError(arrayGet('title', 0)).as('isError'), - ifError(arrayGet('title', 0), constant('was error')).as('ifError'), + equal('rating', null).as('ratingIsNull'), + equal('rating', NaN).as('ratingIsNaN'), + isError(divide(constant(1), constant(0))).as('isError'), + ifError(divide(constant(1), constant(0)), constant('was error')).as( + 'ifError' + ), + ifError( + divide(constant(1), constant(0)).greaterThan(1), + constant(true) + ) + .not() + .as('ifErrorBooleanExpression'), isAbsent('foo').as('isAbsent'), - isNotNull('title').as('titleIsNotNull'), - isNotNan('cost').as('costIsNotNan'), + notEqual('title', null).as('titleIsNotNull'), + notEqual('cost', NaN).as('costIsNotNan'), exists('fooBarBaz').as('fooBarBazExists'), field('title').exists().as('titleExists') ) @@ -1994,6 +2592,7 @@ describe.skip('Firestore Pipelines', () => { ratingIsNaN: false, isError: true, ifError: 'was error', + ifErrorBooleanExpression: false, isAbsent: true, titleIsNotNull: true, costIsNotNan: false, @@ -2008,13 +2607,20 @@ describe.skip('Firestore Pipelines', () => { .sort(field('rating').descending()) .limit(1) .select( - field('rating').isNull().as('ratingIsNull'), - field('rating').isNan().as('ratingIsNaN'), - arrayGet('title', 0).isError().as('isError'), - arrayGet('title', 0).ifError(constant('was error')).as('ifError'), + field('rating').equal(null).as('ratingIsNull'), + field('rating').equal(NaN).as('ratingIsNaN'), + divide(constant(1), constant(0)).isError().as('isError'), + divide(constant(1), constant(0)) + .ifError(constant('was error')) + .as('ifError'), + divide(constant(1), constant(0)) + .greaterThan(1) + .ifError(constant(true)) + .not() + .as('ifErrorBooleanExpression'), field('foo').isAbsent().as('isAbsent'), - field('title').isNotNull().as('titleIsNotNull'), - field('cost').isNotNan().as('costIsNotNan') + field('title').notEqual(null).as('titleIsNotNull'), + field('cost').notEqual(NaN).as('costIsNotNan') ) ); expectResults(snapshot, { @@ -2022,6 +2628,7 @@ describe.skip('Firestore Pipelines', () => { ratingIsNaN: false, isError: true, ifError: 'was error', + ifErrorBooleanExpression: false, isAbsent: true, titleIsNotNull: true, costIsNotNan: false @@ -2048,7 +2655,7 @@ describe.skip('Firestore Pipelines', () => { title: "The Hitchhiker's Guide to the Galaxy", others: { unknown: { year: 1980 } } }, - { hugoAward: true, title: 'Dune', others: null } + { hugoAward: true, title: 'Dune' } ); }); @@ -2141,26 +2748,34 @@ describe.skip('Firestore Pipelines', () => { firestore .pipeline() .collection(randomCol.path) - .where(equal('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('genericFunction', () => { + describe('rawFunction', () => { it('add selectable', async () => { const snapshot = await execute( firestore @@ -2293,7 +2908,7 @@ describe.skip('Firestore Pipelines', () => { }); }); - it('supports arrayOffset', async () => { + it('supports arrayGet', async () => { let snapshot = await execute( firestore .pipeline() @@ -2536,130 +3151,611 @@ describe.skip('Firestore Pipelines', () => { }); }); - it('supports Document_id', async () => { - let snapshot = await execute( + it('can reverse an array', 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(documentId(field('__path__')).as('docId')) + .select(field('tags').arrayReverse().as('reversedTags')) ); expectResults(snapshot, { - docId: 'book4' + reversedTags: ['adventure', 'space', 'comedy'] }); - snapshot = await execute( + }); + + it('can reverse an array 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(field('__path__').documentId().as('docId')) + .select(reverse('tags').as('reversedTags')) ); expectResults(snapshot, { - docId: 'book4' + reversedTags: ['adventure', 'space', 'comedy'] }); }); - it('supports Substr', async () => { - let snapshot = await execute( + it('can compute the ceiling of a numeric 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(substring('title', 9, 2).as('of')) + .select(field('rating').ceil().as('ceilingRating')) ); expectResults(snapshot, { - of: 'of' + 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) - .sort(field('rating').descending()) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(field('title').substring(9, 2).as('of')) + .select(ceil('rating').as('ceilingRating')) ); expectResults(snapshot, { - of: 'of' + ceilingRating: 5 }); }); - it('supports Substr without length', async () => { - let snapshot = await execute( + it('can compute the floor of a numeric 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(substring('title', 9).as('of')) + .select(field('rating').floor().as('floorRating')) ); expectResults(snapshot, { - of: 'of the Rings' + 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) - .sort(field('rating').descending()) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) .limit(1) - .select(field('title').substring(9).as('of')) + .select(floor('rating').as('floorRating')) + ); + expectResults(snapshot, { + floorRating: 4 + }); + }); + + it('can compute e to the power of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(field('rating').exp().as('expRating')) + ); + expect(snapshot.results[0].get('expRating')).to.be.approximately( + 109.94717245212352, + 0.00001 + ); + }); + + it('can compute e to the power of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(exp('rating').as('expRating')) + ); + expect(snapshot.results[0].get('expRating')).to.be.approximately( + 109.94717245212351, + 0.000001 + ); + }); + + it('can compute the power of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').pow(2).as('powerRating')) + ); + expect(snapshot.results[0].get('powerRating')).to.be.approximately( + 17.64, + 0.0001 + ); + }); + + it('can compute the power of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(pow('rating', 2).as('powerRating')) + ); + expect(snapshot.results[0].get('powerRating')).to.be.approximately( + 17.64, + 0.0001 + ); + }); + + it('can round a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(round('rating').as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value away from zero for positive half-way values', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .addFields(constant(1.5).as('positiveHalf')) + .select(field('positiveHalf').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 2 + }); + }); + + it('can round a numeric value away from zero for negative half-way values', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .addFields(constant(-1.5).as('negativeHalf')) + .select(field('negativeHalf').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: -2 + }); + }); + + it('can round a numeric value to specified precision', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + foo: 4.123456 + }) + ) + .select( + field('foo').round(0).as('0'), + round('foo', 1).as('1'), + round('foo', constant(2)).as('2'), + round(field('foo'), 4).as('4') + ) + ); + expectResults(snapshot, { + '0': 4, + '1': 4.1, + '2': 4.12, + '4': 4.1235 + }); + }); + + it('can get the collectionId from a path', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(field('__name__').collectionId().as('collectionId')) + ); + expectResults(snapshot, { + collectionId: randomCol.id + }); + }); + + it('can get the collectionId from a path with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select(collectionId('__name__').as('collectionId')) + ); + expectResults(snapshot, { + collectionId: randomCol.id + }); + }); + + it('can compute the length of a string value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('title').length().as('titleLength')) + ); + expectResults(snapshot, { + titleLength: 36 + }); + }); + + it('can compute the length of a string value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(length('title').as('titleLength')) + ); + expectResults(snapshot, { + titleLength: 36 + }); + }); + + it('can compute the length of an array value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('tags').length().as('tagsLength')) + ); + expectResults(snapshot, { + tagsLength: 3 + }); + }); + + it('can compute the length of an array value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(length('tags').as('tagsLength')) + ); + expectResults(snapshot, { + tagsLength: 3 + }); + }); + + it('can compute the length of a map value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('awards').length().as('awardsLength')) + ); + expectResults(snapshot, { + awardsLength: 3 + }); + }); + + it('can compute the length of a vector value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('embedding').length().as('embeddingLength')) + ); + expectResults(snapshot, { + embeddingLength: 10 + }); + }); + + it('can compute the length of a bytes value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .select(constant('12é').as('value')) + .limit(1) + .select(field('value').byteLength().as('valueLength')) + ); + expectResults(snapshot, { + valueLength: 4 + }); + }); + + it('can compute the natural logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').ln().as('lnRating')) + ); + expect(snapshot.results[0]!.data().lnRating).to.be.closeTo(1.435, 0.001); + }); + + it('can compute the natural logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ln('rating').as('lnRating')) + ); + expect(snapshot.results[0]!.data().lnRating).to.be.closeTo(1.435, 0.001); + }); + + it('can compute the natural logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(ln('rating').as('lnRating')) + ); + expectResults(snapshot, { + lnRating: 1.4350845252893227 + }); + }); + + it('can compute the logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(log(field('rating'), 10).as('logRating')) + ); + expectResults(snapshot, { + logRating: 0.6232492903979004 + }); + }); + + it('can compute the logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(log('rating', 10).as('logRating')) + ); + expectResults(snapshot, { + logRating: 0.6232492903979004 + }); + }); + + it('can round a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').round().as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can round a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(round('rating').as('roundedRating')) + ); + expectResults(snapshot, { + roundedRating: 4 + }); + }); + + it('can compute the square root of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('rating').sqrt().as('sqrtRating')) + ); + expectResults(snapshot, { + sqrtRating: 2.04939015319192 + }); + }); + + it('can compute the square root of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(sqrt('rating').as('sqrtRating')) + ); + expectResults(snapshot, { + sqrtRating: 2.04939015319192 + }); + }); + + it('can reverse a string', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(field('title').reverse().as('reversedTitle')) + ); + expectResults(snapshot, { + reversedTitle: "yxalaG eht ot ediuG s'rekihhctiH ehT" + }); + }); + + it('can reverse a string with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal("The Hitchhiker's Guide to the Galaxy")) + .limit(1) + .select(stringReverse('title').as('reversedTitle')) + ); + expectResults(snapshot, { + reversedTitle: "yxalaG eht ot ediuG s'rekihhctiH ehT" + }); + }); + + it('supports Document_id', async () => { + let snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select( + documentId(field('__name__')).as('docId'), + documentId(field('__path__')).as('noDocId') + ) + ); + expectResults(snapshot, { + docId: 'book4', + noDocId: null + }); + snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .sort(field('rating').descending()) + .limit(1) + .select(field('__name__').documentId().as('docId')) ); expectResults(snapshot, { - of: 'of the Rings' + docId: 'book4' }); }); - it('arrayConcat works', async () => { - const snapshot = await execute( + it('supports substring', async () => { + let snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .select( - arrayConcat('tags', ['newTag1', 'newTag2'], field('tags'), [ - null - ]).as('modifiedTags') - ) + .sort(field('rating').descending()) .limit(1) + .select(substring('title', 9, 2).as('of')) ); expectResults(snapshot, { - modifiedTags: [ - 'comedy', - 'space', - 'adventure', - 'newTag1', - 'newTag2', - 'comedy', - 'space', - 'adventure', - null - ] + 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('testToLowercase', async () => { + it('test toLower', async () => { const snapshot = await execute( firestore .pipeline() .collection(randomCol.path) - .select(toLower('title').as('lowercaseTitle')) + .sort(ascending('title')) + .select(toLower('author').as('lowercaseAuthor')) .limit(1) ); expectResults(snapshot, { - lowercaseTitle: "the hitchhiker's guide to the galaxy" + lowercaseAuthor: 'george orwell' }); }); - it('testToUppercase', async () => { + 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: 'DOUGLAS ADAMS' }); + expectResults(snapshot, { uppercaseAuthor: 'GEORGE ORWELL' }); }); it('testTrim', async () => { @@ -2688,11 +3784,179 @@ describe.skip('Firestore Pipelines', () => { .limit(1) .select(reverse('title').as('reverseTitle')) ); - expectResults(snapshot, { title: '4891' }); + expectResults(snapshot, { reverseTitle: '4891' }); + }); + + it('testAbs', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .select( + constant(-10).as('neg10'), + constant(-22.22).as('neg22'), + constant(1).as('pos1') + ) + .select( + abs('neg10').as('10'), + abs(field('neg22')).as('22'), + field('pos1').as('1') + ) + ); + expectResults(snapshot, { + '10': 10, + '22': 22.22, + '1': 1 + }); + }); + + it('can compute the base-10 logarithm of a numeric value', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(field('rating').log10().as('log10Rating')) + ); + expect(snapshot.results[0]!.data().log10Rating).to.be.closeTo( + 0.672, + 0.001 + ); + }); + + it('can compute the base-10 logarithm of a numeric value with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .select(log10('rating').as('log10Rating')) + ); + expect(snapshot.results[0]!.data().log10Rating).to.be.closeTo( + 0.672, + 0.001 + ); + }); + + it('can concat fields', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .addFields( + concat('author', ' ', field('title')).as('display'), + field('author').concat(': ', field('title')).as('display2') + ) + .where(equal('author', 'Douglas Adams')) + .select('display', 'display2') + ); + expectResults(snapshot, { + display: "Douglas Adams The Hitchhiker's Guide to the Galaxy", + display2: "Douglas Adams: The Hitchhiker's Guide to the Galaxy" + }); + }); + + it('supports currentTimestamp', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .addFields(currentTimestamp().as('now')) + .select('now') + ); + const now = snapshot.results[0].get('now') as Timestamp; + expect(now).instanceof(Timestamp); + expect( + now.toDate().getUTCSeconds() - new Date().getUTCSeconds() + ).lessThan(5000); + }); + + it('supports ifAbsent', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + title: 'foo' + }) + ) + .select( + ifAbsent('title', 'default title').as('title'), + field('name').ifAbsent('default name').as('name'), + field('name').ifAbsent(field('title')).as('nameOrTitle') + ) + ); + + expectResults(snapshot, { + title: 'foo', + name: 'default name', + nameOrTitle: 'foo' + }); + }); + + it('supports join', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .limit(1) + .replaceWith( + map({ + tags: ['foo', 'bar', 'baz'], + delimeter: '|' + }) + ) + .select(join('tags', ',').as('csv'), field('tags').join('|').as('or')) + ); + + expectResults(snapshot, { + csv: 'foo,bar,baz', + or: 'foo|bar|baz' + }); + }); + + it('can compute the sum of the elements in an array', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .addFields(array([150, 200]).as('sales')) + .select(field('sales').arraySum().as('totalSales')) + ); + expectResults(snapshot, { + totalSales: 350 + }); + }); + + it('can compute the sum of the elements in an array with the top-level function', async () => { + const snapshot = await execute( + firestore + .pipeline() + .collection(randomCol.path) + .where(field('title').equal('The Lord of the Rings')) + .limit(1) + .addFields(array([150, 200]).as('sales')) + .select(arraySum('sales').as('totalSales')) + ); + expectResults(snapshot, { + totalSales: 350 + }); }); + + // TODO(new-expression): Add new expression tests above this line }); describe('pagination', () => { + let addedDocs: DocumentReference[] = []; + /** * Adds several books to the test collection. These * additional books support pagination test scenarios @@ -2703,7 +3967,9 @@ describe.skip('Firestore Pipelines', () => { 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', @@ -2712,7 +3978,9 @@ describe.skip('Firestore Pipelines', () => { 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', @@ -2726,7 +3994,9 @@ describe.skip('Firestore Pipelines', () => { ], 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', @@ -2737,108 +4007,135 @@ describe.skip('Firestore Pipelines', () => { }); } - // sort on __name__ is not working - itIf(testUnsupportedFeatures)( - 'supports pagination with filters', - async () => { - await addBooks(randomCol); - const pageSize = 2; - const pipeline = firestore - .pipeline() - .collection(randomCol.path) - .select('title', 'rating', '__name__') - .sort(field('rating').descending(), field('__name__').ascending()); + 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').equal(lastDoc.get('rating')), - field('__path__').greaterThan(lastDoc.ref?.id) - ), - field('rating').lessThan(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 - 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) + snapshot = await execute( + pipeline.offset(currPage++ * pageSize).limit(pageSize) + ); + expectResults( + snapshot, + { + title: 'A Long Way to a Small, Angry Planet', + rating: 4.6 + }, + { + title: 'Pride and Prejudice', + rating: 4.5 + } + ); + }); + }); + + describe('stage options', () => { + describe('forceIndex', () => { + // SKIP: requires pre-existing index + // eslint-disable-next-line no-restricted-properties + it.skip('Collection Stage', async () => { + const snapshot = await execute( + firestore.pipeline().collection({ + collection: randomCol, + forceIndex: 'unknown' + }) ); - expectResults( - snapshot, - { - title: 'A Long Way to a Small, Angry Planet', - rating: 4.6 - }, - { - title: 'Pride and Prejudice', - rating: 4.5 - } + expect(snapshot.results.length).to.equal(10); + }); + + // SKIP: requires pre-existing index + // eslint-disable-next-line no-restricted-properties + it.skip('CollectionGroup Stage', async () => { + const snapshot = await execute( + firestore.pipeline().collectionGroup({ + collectionId: randomCol.id, + forceIndex: 'unknown' + }) ); - } - ); + expect(snapshot.results.length).to.equal(10); + }); + }); }); }); diff --git a/packages/firestore/test/lite/pipeline_export.ts b/packages/firestore/test/lite/pipeline_export.ts new file mode 100644 index 00000000000..d5f60db9d30 --- /dev/null +++ b/packages/firestore/test/lite/pipeline_export.ts @@ -0,0 +1,24 @@ +/** + * @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. + */ + +// Imports firebase via the raw sources and re-exports it. The +// "/integration/firestore" test suite replaces this file with a +// reference to the minified sources. If you change any exports in this file, +// you need to also adjust "integration/firestore/pipeline_export.ts". + +// @ts-ignore +export * from '../../lite/pipelines/pipelines.ts'; diff --git a/packages/firestore/test/unit/remote/fetch_connection.test.ts b/packages/firestore/test/unit/remote/fetch_connection.test.ts index 5a9aa67436f..4de0ba5a722 100644 --- a/packages/firestore/test/unit/remote/fetch_connection.test.ts +++ b/packages/firestore/test/unit/remote/fetch_connection.test.ts @@ -43,6 +43,7 @@ describe('Fetch Connection', () => { DatabaseId.empty(), '', '', + '', new FirestoreSettingsImpl({ host: 'abc.cloudworkstations.dev' }) diff --git a/packages/firestore/test/unit/remote/grpc_connection.node.test.ts b/packages/firestore/test/unit/remote/grpc_connection.node.test.ts new file mode 100644 index 00000000000..8364cbbd5e6 --- /dev/null +++ b/packages/firestore/test/unit/remote/grpc_connection.node.test.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 { Metadata } from '@grpc/grpc-js'; +import { expect } from 'chai'; + +import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info'; +import { ResourcePath } from '../../../src/model/path'; +import { GrpcConnection } from '../../../src/platform/node/grpc_connection'; + +export class TestGrpcConnection extends GrpcConnection { + mockStub = { + lastMetadata: null, + mockRpc( + req: unknown, + metadata: Metadata, + callback: (err: unknown, resp: unknown) => void + ) { + this.lastMetadata = metadata; + callback(null, null); + } + } as { + lastMetadata: null | Metadata; + [index: string]: unknown; + }; + + protected ensureActiveStub(): unknown { + return this.mockStub; + } +} + +describe('GrpcConnection', () => { + const testDatabaseInfo = new DatabaseInfo( + new DatabaseId('testproject'), + 'test-app-id', + 'persistenceKey', + 'example.com', + /*ssl=*/ false, + /*forceLongPolling=*/ false, + /*autoDetectLongPolling=*/ false, + /*longPollingOptions=*/ {}, + /*useFetchStreams=*/ false, + /*isUsingEmulator=*/ false, + 'grpc-connection-test-api-key' + ); + const connection = new TestGrpcConnection( + { google: { firestore: { v1: {} } } }, + testDatabaseInfo + ); + + it('Passes the API Key from DatabaseInfo to the grpc stub', async () => { + const request = { + database: 'projects/testproject/databases/(default)', + writes: [] + }; + await connection.invokeRPC( + 'mockRpc', + ResourcePath.emptyPath(), + request, + null, + null + ); + expect( + connection.mockStub.lastMetadata?.get('x-goog-api-key') + ).to.deep.equal(['grpc-connection-test-api-key']); + }); +}); diff --git a/packages/firestore/test/unit/remote/rest_connection.test.ts b/packages/firestore/test/unit/remote/rest_connection.test.ts index 100b8b8368e..3501a910f66 100644 --- a/packages/firestore/test/unit/remote/rest_connection.test.ts +++ b/packages/firestore/test/unit/remote/rest_connection.test.ts @@ -68,7 +68,8 @@ describe('RestConnection', () => { /*autoDetectLongPolling=*/ false, /*longPollingOptions=*/ {}, /*useFetchStreams=*/ false, - /*isUsingEmulator=*/ false + /*isUsingEmulator=*/ false, + 'rest-connection-test-api-key' ); const connection = new TestRestConnection(testDatabaseInfo); @@ -83,7 +84,7 @@ describe('RestConnection', () => { null ); expect(connection.lastUrl).to.equal( - 'http://example.com/v1/projects/testproject/databases/(default)/documents:commit' + 'http://example.com/v1/projects/testproject/databases/(default)/documents:commit?key=rest-connection-test-api-key' ); }); diff --git a/packages/firestore/test/unit/remote/web_channel_connection.browser.test.ts b/packages/firestore/test/unit/remote/web_channel_connection.browser.test.ts new file mode 100644 index 00000000000..6558fb84dea --- /dev/null +++ b/packages/firestore/test/unit/remote/web_channel_connection.browser.test.ts @@ -0,0 +1,70 @@ +/** + * @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 { + WebChannelOptions, + WebChannelTransport +} from '@firebase/webchannel-wrapper'; +import { expect } from 'chai'; + +import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info'; +import { WebChannelConnection } from '../../../src/platform/browser/webchannel_connection'; + +export class TestWebChannelConnection extends WebChannelConnection { + transport: { lastOptions?: WebChannelOptions } & WebChannelTransport = { + lastOptions: undefined, + createWebChannel(url: string, options: WebChannelOptions): never { + this.lastOptions = options; + + // Throw here so we don't have to mock out any more of Web Channel + throw new Error('Not implemented for test'); + } + }; + protected createWebChannelTransport(): WebChannelTransport { + return this.transport; + } +} + +describe('WebChannelConnection', () => { + const testDatabaseInfo = new DatabaseInfo( + new DatabaseId('testproject'), + 'test-app-id', + 'persistenceKey', + 'example.com', + /*ssl=*/ false, + /*forceLongPolling=*/ false, + /*autoDetectLongPolling=*/ false, + /*longPollingOptions=*/ {}, + /*useFetchStreams=*/ false, + /*isUsingEmulator=*/ false, + 'wc-connection-test-api-key' + ); + + it('Passes the API Key from DatabaseInfo to makeHeaders for openStream', async () => { + const connection = new TestWebChannelConnection(testDatabaseInfo); + + expect(() => connection.openStream('mockRpc', null, null)).to.throw( + 'Not implemented for test' + ); + + const headers = connection.transport.lastOptions + ?.initMessageHeaders as unknown as { [key: string]: string }; + expect(headers['x-goog-api-key']).to.deep.equal( + 'wc-connection-test-api-key' + ); + }); +}); diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index 51d2229b8a1..50806cb2a48 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -283,7 +283,8 @@ abstract class TestRunner { /*autoDetectLongPolling=*/ false, /*longPollingOptions=*/ {}, /*useFetchStreams=*/ false, - /*isUsingEmulator=*/ false + /*isUsingEmulator=*/ false, + 'test-api-key' ); // TODO(mrschmidt): During client startup in `firestore_client`, we block diff --git a/packages/firestore/test/util/mocha_extensions.ts b/packages/firestore/test/util/mocha_extensions.ts new file mode 100644 index 00000000000..6ea22cbc36f --- /dev/null +++ b/packages/firestore/test/util/mocha_extensions.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable no-restricted-properties */ + +import { USE_EMULATOR } from '../integration/util/settings'; + +// Helper to make a type itselt (T) and optionally union that with (T['skip']) +type tOrSkipT = T | (T extends { skip: unknown } ? T['skip'] : T); + +interface ExtendMochaTypeWithHelpers { + // Declare helpers + skipEmulator: tOrSkipT; + skipEnterprise: tOrSkipT; + skipClassic: tOrSkipT; +} + +declare module 'mocha' { + // TODO add mocha types that must be extended + interface TestFunction extends ExtendMochaTypeWithHelpers {} + interface PendingTestFunction + extends ExtendMochaTypeWithHelpers {} + interface SuiteFunction extends ExtendMochaTypeWithHelpers {} + interface PendingSuiteFunction + extends ExtendMochaTypeWithHelpers {} +} + +// Define helpers +export function mixinSkipImplementations(obj: unknown): void { + Object.defineProperty(obj, 'skipEmulator', { + get(): unknown { + if (this === it.skip) { + return this; + } + if (this === describe.skip) { + return this; + } + if (USE_EMULATOR) { + return this.skip; + } + return this; + } + }); + + Object.defineProperty(obj, 'skipEnterprise', { + get(): unknown { + if (this === it.skip) { + return this; + } + if (this === describe.skip) { + return this; + } + if (process.env.RUN_ENTERPRISE_TESTS) { + return this.skip; + } + return this; + } + }); + + Object.defineProperty(obj, 'skipClassic', { + get(): unknown { + if (this === it.skip) { + return this; + } + if (this === describe.skip) { + return this; + } + if (!process.env.RUN_ENTERPRISE_TESTS) { + return this.skip; + } + return this; + } + }); +} + +// TODO add mocha functions that must be extended +[global.it, global.it.skip, global.describe, global.describe.skip].forEach( + mixinSkipImplementations +); + +// Export modified it and describe. +const it = global.it; +const describe = global.describe; +export { it, describe }; diff --git a/packages/functions-compat/package.json b/packages/functions-compat/package.json index 00f065b2b34..c64d29d4eaf 100644 --- a/packages/functions-compat/package.json +++ b/packages/functions-compat/package.json @@ -29,7 +29,7 @@ "@firebase/app-compat": "0.x" }, "devDependencies": { - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/functions/package.json b/packages/functions/package.json index 99f5059fb0d..4a85faa9858 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -49,7 +49,7 @@ "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/installations-compat/package.json b/packages/installations-compat/package.json index c5ff66cd0d5..20e21e0d8a6 100644 --- a/packages/installations-compat/package.json +++ b/packages/installations-compat/package.json @@ -44,7 +44,7 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/installations/package.json b/packages/installations/package.json index ced645951aa..9ae0bb0a097 100644 --- a/packages/installations/package.json +++ b/packages/installations/package.json @@ -49,7 +49,7 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/messaging-compat/package.json b/packages/messaging-compat/package.json index 2279cce7c2b..92a6f8fba1c 100644 --- a/packages/messaging-compat/package.json +++ b/packages/messaging-compat/package.json @@ -44,7 +44,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.5.4", + "@firebase/app-compat": "0.5.5", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", "ts-essentials": "9.4.2", diff --git a/packages/messaging/package.json b/packages/messaging/package.json index 12a85d5045d..bd87ae31fc5 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -60,7 +60,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/performance-compat/package.json b/packages/performance-compat/package.json index b0d17ef5899..94050c1ed31 100644 --- a/packages/performance-compat/package.json +++ b/packages/performance-compat/package.json @@ -51,7 +51,7 @@ "rollup-plugin-replace": "2.2.0", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4", - "@firebase/app-compat": "0.5.4" + "@firebase/app-compat": "0.5.5" }, "repository": { "directory": "packages/performance-compat", diff --git a/packages/performance/package.json b/packages/performance/package.json index 302a33531cc..125d3f084f5 100644 --- a/packages/performance/package.json +++ b/packages/performance/package.json @@ -47,7 +47,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/remote-config-compat/package.json b/packages/remote-config-compat/package.json index 07180167c40..dbf19c1830f 100644 --- a/packages/remote-config-compat/package.json +++ b/packages/remote-config-compat/package.json @@ -50,7 +50,7 @@ "rollup-plugin-replace": "2.2.0", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4", - "@firebase/app-compat": "0.5.4" + "@firebase/app-compat": "0.5.5" }, "repository": { "directory": "packages/remote-config-compat", diff --git a/packages/remote-config/package.json b/packages/remote-config/package.json index 74cdac97568..d02f746ed23 100644 --- a/packages/remote-config/package.json +++ b/packages/remote-config/package.json @@ -48,7 +48,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "rollup-plugin-dts": "5.3.1", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/storage-compat/package.json b/packages/storage-compat/package.json index 0ba6213cfa4..19a5f352b71 100644 --- a/packages/storage-compat/package.json +++ b/packages/storage-compat/package.json @@ -44,8 +44,8 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.5.4", - "@firebase/auth-compat": "0.6.0", + "@firebase/app-compat": "0.5.5", + "@firebase/auth-compat": "0.6.1", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/storage/package.json b/packages/storage/package.json index 312f284fddf..2b0df30704f 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -54,8 +54,8 @@ "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.14.4", - "@firebase/auth": "1.11.0", + "@firebase/app": "0.14.5", + "@firebase/auth": "1.11.1", "rollup": "2.79.2", "@rollup/plugin-alias": "5.1.1", "@rollup/plugin-json": "6.1.0", diff --git a/packages/template/package.json b/packages/template/package.json index bad73496caa..635f4f451c5 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -48,7 +48,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/repo-scripts/prune-dts/prune-dts.ts b/repo-scripts/prune-dts/prune-dts.ts index 087d12a3d4b..bbfad0a3f3c 100644 --- a/repo-scripts/prune-dts/prune-dts.ts +++ b/repo-scripts/prune-dts/prune-dts.ts @@ -564,6 +564,21 @@ function dropPrivateApiTransformer( return (sourceFile: ts.SourceFile) => { const imports: Record> = {}; + // Get exported symbols + const directExportedSymbols = typeChecker.getExportsOfModule( + typeChecker.getSymbolAtLocation(sourceFile)! + ); + // Map exported symbols to aliases. + // For the statement `export { X as Y };`, this list would contain a symbol + // for `X`. + const aliasedExportedSymbols = directExportedSymbols + .map(symbol => + symbol.flags & ts.SymbolFlags.Alias + ? typeChecker.getAliasedSymbol(symbol) + : undefined + ) + .filter(symbol => symbol !== undefined); + function ensureImportsForFile(filename: string): Array { let importsForFile = imports[filename]; if (!importsForFile) { @@ -584,12 +599,27 @@ function dropPrivateApiTransformer( ts.isEnumDeclaration(node) ) { // Remove any types that are not exported. + // First we check the modifiers for the symbol `export function X`. If + // the export keyword is found, the symbol is modified. + // Second we check if the symbol has an alias that is exported elsewhere, + // for example: `function X; export { X as Y }`. If the alias is + // exported elsewhere, then we also have to keep the symbol. if ( !ts .getModifiers(node) ?.find(m => m.kind === ts.SyntaxKind.ExportKeyword) ) { - return ts.factory.createNotEmittedStatement(node); + // Try to get a symbol for this node. + const symbol = + 'name' in node && node.name + ? typeChecker.getSymbolAtLocation(node.name) + : undefined; + // Check if that symbol is in the list of aliased exported symbols. + // If it is, we keep the symbol. Otherwise, we remove the symbol. + if (!symbol || !aliasedExportedSymbols.includes(symbol)) { + // NO-OP block to keep the condition readable + return ts.factory.createNotEmittedStatement(node); + } } } diff --git a/repo-scripts/size-analysis/package.json b/repo-scripts/size-analysis/package.json index 162a491eac7..78a2f87e21c 100644 --- a/repo-scripts/size-analysis/package.json +++ b/repo-scripts/size-analysis/package.json @@ -40,7 +40,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@firebase/app": "0.14.4", + "@firebase/app": "0.14.5", "@firebase/logger": "0.5.0", "@types/webpack": "5.28.5" }, diff --git a/scripts/emulator-testing/emulators/dataconnect-emulator.ts b/scripts/emulator-testing/emulators/dataconnect-emulator.ts index 9dc6add5df1..729889364d1 100644 --- a/scripts/emulator-testing/emulators/dataconnect-emulator.ts +++ b/scripts/emulator-testing/emulators/dataconnect-emulator.ts @@ -18,7 +18,7 @@ import { platform } from 'os'; import { Emulator } from './emulator'; -const DATACONNECT_EMULATOR_VERSION = '1.9.2'; +const DATACONNECT_EMULATOR_VERSION = '2.15.1'; export class DataConnectEmulator extends Emulator { constructor(port = 9399) { diff --git a/scripts/size_report/report_binary_size.ts b/scripts/size_report/report_binary_size.ts index da1ad166702..34dce01924e 100644 --- a/scripts/size_report/report_binary_size.ts +++ b/scripts/size_report/report_binary_size.ts @@ -57,7 +57,7 @@ function generateReportForCDNScripts(): Report[] { ...special_files.map((file: string) => `${firebaseRoot}/${file}`), ...pkgJson.components.map( (component: string) => - `${firebaseRoot}/firebase-${component.replace('/', '-')}.js` + `${firebaseRoot}/firebase-${component.replaceAll('/', '-')}.js` ), ...compatPkgJson.components.map( (component: string) => `${firebaseRoot}/firebase-${component}-compat.js` diff --git a/scripts/update_vertexai_responses.sh b/scripts/update_vertexai_responses.sh index f6557ab0c0a..d32a7c83fc6 100755 --- a/scripts/update_vertexai_responses.sh +++ b/scripts/update_vertexai_responses.sh @@ -17,7 +17,7 @@ # This script replaces mock response files for Vertex AI unit tests with a fresh # clone of the shared repository of Vertex AI test data. -RESPONSES_VERSION='v14.*' # The major version of mock responses to use +RESPONSES_VERSION='v15.*' # The major version of mock responses to use REPO_NAME="vertexai-sdk-test-data" REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git"