From db1229571af8671c3ae1f1f6361145c878c95837 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 12 May 2026 14:42:41 +0100 Subject: [PATCH 1/6] feat(firestore): arrayFirst and arrayFirstN API features --- .../firestore/lib/pipelines/expressions.ts | 32 +++++++++++++++++++ packages/firestore/lib/pipelines/index.ts | 2 ++ 2 files changed, 34 insertions(+) diff --git a/packages/firestore/lib/pipelines/expressions.ts b/packages/firestore/lib/pipelines/expressions.ts index d85a7fa9c5..a10c029ea6 100644 --- a/packages/firestore/lib/pipelines/expressions.ts +++ b/packages/firestore/lib/pipelines/expressions.ts @@ -110,6 +110,9 @@ interface FluentExpressionMethods { arrayAgg(): AggregateFunction; arrayAggDistinct(): AggregateFunction; arrayFilter(alias: string, filter: BooleanExpression): FunctionExpression; + arrayFirst(): FunctionExpression; + arrayFirstN(n: number): FunctionExpression; + arrayFirstN(n: Expression): FunctionExpression; } export interface BooleanExpression extends Selectable, FluentExpressionMethods { @@ -397,6 +400,8 @@ const EXPRESSION_METHOD_NAMES = [ 'arrayContainsAny', 'arrayContainsAll', 'arrayFilter', + 'arrayFirst', + 'arrayFirstN', 'startsWith', 'endsWith', 'add', @@ -719,6 +724,8 @@ function normalizeGlobalArguments(name: string, args: unknown[]): RuntimeNode[] case 'toUpper': case 'trim': case 'substring': + case 'arrayFirst': + case 'arrayFirstN': case 'arrayGet': case 'arrayLength': case 'arraySum': @@ -1057,6 +1064,31 @@ export function arrayFilter( return callFunctionHelper('arrayFilter', arguments); } +/** + * @beta + * Gets the first element in an array field or expression. + */ +export function arrayFirst(_arrayField: string): FunctionExpression; +export function arrayFirst(_arrayExpression: Expression): FunctionExpression; +export function arrayFirst(_arrayOrField: string | Expression): FunctionExpression { + return callFunctionHelper('arrayFirst', arguments); +} + +/** + * @beta + * Gets the first `n` elements in an array field or expression. + */ +export function arrayFirstN(_arrayField: string, _n: number): FunctionExpression; +export function arrayFirstN(_arrayField: string, _n: Expression): FunctionExpression; +export function arrayFirstN(_arrayExpression: Expression, _n: number): FunctionExpression; +export function arrayFirstN(_arrayExpression: Expression, _n: Expression): FunctionExpression; +export function arrayFirstN( + _arrayOrField: string | Expression, + _n: number | Expression, +): FunctionExpression { + return callFunctionHelper('arrayFirstN', arguments); +} + /** * @beta * Checks if a string starts with a prefix. diff --git a/packages/firestore/lib/pipelines/index.ts b/packages/firestore/lib/pipelines/index.ts index 10d8fbda0f..eaefca109a 100644 --- a/packages/firestore/lib/pipelines/index.ts +++ b/packages/firestore/lib/pipelines/index.ts @@ -81,6 +81,8 @@ export { arrayContainsAny, arrayContainsAll, arrayFilter, + arrayFirst, + arrayFirstN, startsWith, endsWith, OrderingHelper as Ordering, From bffef0463bd466084e31a577f8ca892da0cdf8d6 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 12 May 2026 14:43:16 +0100 Subject: [PATCH 2/6] feat(firestore, ios): arrayFirst and arrayFirstN API features --- .../RNFBFirestorePipelineNodeBuilder.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineNodeBuilder.swift b/packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineNodeBuilder.swift index 69937fbd0e..49ba8fa27f 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineNodeBuilder.swift +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineNodeBuilder.swift @@ -747,6 +747,10 @@ final class RNFBFirestorePipelineNodeBuilder { return "array_contains_any" case "arraycontainsall": return "array_contains_all" + case "arrayfirst": + return "array_first" + case "arrayfirstn": + return "array_first_n" case "charlength", "characterlength": return "char_length" case "bytelength": @@ -1142,6 +1146,31 @@ final class RNFBFirestorePipelineNodeBuilder { break expressionLoop } + if normalized == "arrayfirst" { + guard rawArgs.count == 1 else { + throw PipelineValidationError( + "pipelineExecute() expected \(currentField).\(name) to include exactly 1 argument.") + } + + let argBoxes = rawArgs.map { _ in ExprBridgeBox() } + stack.append(.functionExit(box, "array_first", argBoxes, currentField)) + stack.append(.enter(rawArgs[0], "\(currentField).args[0]", .expressionValue, argBoxes[0])) + break expressionLoop + } + + if normalized == "arrayfirstn" { + guard rawArgs.count == 2 else { + throw PipelineValidationError( + "pipelineExecute() expected \(currentField).\(name) to include exactly 2 arguments.") + } + + let argBoxes = rawArgs.map { _ in ExprBridgeBox() } + stack.append(.functionExit(box, "array_first_n", argBoxes, currentField)) + stack.append(.enter(rawArgs[1], "\(currentField).args[1]", .expressionValue, argBoxes[1])) + stack.append(.enter(rawArgs[0], "\(currentField).args[0]", .expressionValue, argBoxes[0])) + break expressionLoop + } + if normalized == "conditional" { guard rawArgs.count == 3 else { throw PipelineValidationError( From 899506f43013569acd11a43fdeadf2e866bd7062 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 12 May 2026 14:43:24 +0100 Subject: [PATCH 3/6] feat(firestore, android): arrayFirst and arrayFirstN API features --- ...eFirebaseFirestorePipelineNodeBuilder.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestorePipelineNodeBuilder.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestorePipelineNodeBuilder.java index e83b311e30..c8ee251d88 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestorePipelineNodeBuilder.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestorePipelineNodeBuilder.java @@ -409,6 +409,27 @@ private static final class ExitReceiverArrayGetFrame implements ObjectLoweringFr } } + private static final class ExitReceiverArrayFirstNFrame implements ObjectLoweringFrame { + final LoweredExpressionBox box; + final List pendingOperations; + final int nextIndex; + final Expression currentExpression; + final LoweredExpressionBox countBox; + + ExitReceiverArrayFirstNFrame( + LoweredExpressionBox box, + List pendingOperations, + int nextIndex, + Expression currentExpression, + LoweredExpressionBox countBox) { + this.box = box; + this.pendingOperations = pendingOperations; + this.nextIndex = nextIndex; + this.currentExpression = currentExpression; + this.countBox = countBox; + } + } + private static final class ExitReceiverArrayConcatFrame implements ObjectLoweringFrame { final LoweredExpressionBox box; final List pendingOperations; @@ -1556,6 +1577,36 @@ private void processObjectLoweringStack(ArrayDeque stack) indexArg, operationFieldName + ".args[1]", indexBox)); continue; } + case "arrayfirstn": + { + Object countArg = args.get(1); + if (!containsLowerableExpression(countArg)) { + Object countValue = resolveConstantValue(countArg, operationFieldName + ".args[1]"); + if (countValue instanceof Number) { + stack.push( + new ContinueReceiverExpressionChainFrame( + continueFrame.box, + null, + continueFrame.pendingOperations, + nextIndex, + currentExpression.arrayFirstN(((Number) countValue).intValue()))); + continue; + } + } + + LoweredExpressionBox countBox = new LoweredExpressionBox(); + stack.push( + new ExitReceiverArrayFirstNFrame( + continueFrame.box, + continueFrame.pendingOperations, + nextIndex, + currentExpression, + countBox)); + stack.push( + new EnterObjectExpressionValueFrame( + countArg, operationFieldName + ".args[1]", countBox)); + continue; + } case "arrayconcat": { if (args.size() < 2) { @@ -1787,6 +1838,18 @@ private void processObjectLoweringStack(ArrayDeque stack) continue; } + if (frame instanceof ExitReceiverArrayFirstNFrame) { + ExitReceiverArrayFirstNFrame exitFrame = (ExitReceiverArrayFirstNFrame) frame; + stack.push( + new ContinueReceiverExpressionChainFrame( + exitFrame.box, + null, + exitFrame.pendingOperations, + exitFrame.nextIndex, + exitFrame.currentExpression.arrayFirstN(exitFrame.countBox.value))); + continue; + } + if (frame instanceof ExitReceiverArrayConcatFrame) { ExitReceiverArrayConcatFrame exitFrame = (ExitReceiverArrayConcatFrame) frame; Object secondValue = exitFrame.childBoxes.get(0).value; @@ -2571,6 +2634,7 @@ private boolean isDeferredUnaryExpressionFunction(String normalizedFunctionName) return "type".equals(normalizedFunctionName) || "collectionid".equals(normalizedFunctionName) || "documentid".equals(normalizedFunctionName) + || "arrayfirst".equals(normalizedFunctionName) || "arraylength".equals(normalizedFunctionName) || "arraysum".equals(normalizedFunctionName) || "vectorlength".equals(normalizedFunctionName) @@ -2588,6 +2652,7 @@ private boolean isDeferredReceiverExpressionFunction(String normalizedFunctionNa || "mapget".equals(normalizedFunctionName) || "mapmerge".equals(normalizedFunctionName) || "arrayget".equals(normalizedFunctionName) + || "arrayfirstn".equals(normalizedFunctionName) || "arrayconcat".equals(normalizedFunctionName) || "cosinedistance".equals(normalizedFunctionName) || "dotproduct".equals(normalizedFunctionName) @@ -2612,6 +2677,9 @@ private Expression applyPendingUnaryExpressionFunctions( case "documentid": currentExpression = currentExpression.documentId(); break; + case "arrayfirst": + currentExpression = currentExpression.arrayFirst(); + break; case "arraylength": currentExpression = currentExpression.arrayLength(); break; @@ -2758,6 +2826,12 @@ private Expression buildSpecialParsedExpressionFunction( case "arrayget": requireParsedArgumentCount(args, 2, functionName, fieldName); return buildParsedArrayGetExpression(args, fieldName); + case "arrayfirst": + requireParsedArgumentCount(args, 1, functionName, fieldName); + return coerceExpressionValueNode(args.get(0), fieldName + ".args[0]").arrayFirst(); + case "arrayfirstn": + requireParsedArgumentCount(args, 2, functionName, fieldName); + return buildParsedArrayFirstNExpression(args, fieldName); case "arrayconcat": return buildParsedArrayConcatExpression(args, functionName, fieldName); case "arraysum": @@ -2975,6 +3049,19 @@ private Expression buildParsedArrayGetExpression( return arrayExpr.arrayGet(coerceExpressionValueNode(args.get(1), fieldName + ".args[1]")); } + private Expression buildParsedArrayFirstNExpression( + List args, String fieldName) + throws ReactNativeFirebaseFirestorePipelineExecutor.PipelineValidationException { + Expression arrayExpr = coerceExpressionValueNode(args.get(0), fieldName + ".args[0]"); + if (!containsParsedExpression(args.get(1))) { + Object countValue = resolveValueNode(args.get(1), fieldName + ".args[1]"); + if (countValue instanceof Number) { + return arrayExpr.arrayFirstN(((Number) countValue).intValue()); + } + } + return arrayExpr.arrayFirstN(coerceExpressionValueNode(args.get(1), fieldName + ".args[1]")); + } + private Expression buildParsedArrayConcatExpression( List args, String functionName, From 4821f25520af201524d4daa383e26321afca7fc0 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 12 May 2026 14:43:44 +0100 Subject: [PATCH 4/6] test(firestore): arrayFirst and arrayFirstN API features --- .../__tests__/pipelines-parity.test.ts | 18 +++++ .../firestore/__tests__/pipelines.test.ts | 74 +++++++++++++++++++ packages/firestore/consumer-type-test.ts | 17 +++++ packages/firestore/e2e/Pipeline.e2e.js | 12 ++- packages/firestore/type-test.ts | 17 +++++ 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/packages/firestore/__tests__/pipelines-parity.test.ts b/packages/firestore/__tests__/pipelines-parity.test.ts index ca749cdbcb..1402f681a1 100644 --- a/packages/firestore/__tests__/pipelines-parity.test.ts +++ b/packages/firestore/__tests__/pipelines-parity.test.ts @@ -13,10 +13,18 @@ const ANDROID_EXECUTOR_PATH = join( ROOT, 'packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestorePipelineParser.java', ); +const ANDROID_NODE_BUILDER_PATH = join( + ROOT, + 'packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestorePipelineNodeBuilder.java', +); const IOS_EXECUTOR_PATH = join( ROOT, 'packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineParser.swift', ); +const IOS_NODE_BUILDER_PATH = join( + ROOT, + 'packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineNodeBuilder.swift', +); function extractQuotedList(source: string, marker: string, endMarker: string): string[] { const markerIndex = source.indexOf(marker); @@ -79,4 +87,14 @@ describe('Firestore pipeline native parity', function () { expect(iosSource).toContain('does not support options.rawOptions on iOS'); expect(iosSource).toContain('does not support pipeline.source.rawOptions'); }); + + it('keeps arrayFirst and arrayFirstN on native lowering paths', function () { + const androidSource = readFileSync(ANDROID_NODE_BUILDER_PATH, 'utf8'); + const iosSource = readFileSync(IOS_NODE_BUILDER_PATH, 'utf8'); + + expect(androidSource).toContain('currentExpression.arrayFirst()'); + expect(androidSource).toContain('arrayExpr.arrayFirstN'); + expect(iosSource).toContain('"array_first"'); + expect(iosSource).toContain('"array_first_n"'); + }); }); diff --git a/packages/firestore/__tests__/pipelines.test.ts b/packages/firestore/__tests__/pipelines.test.ts index d368043784..09623b5765 100644 --- a/packages/firestore/__tests__/pipelines.test.ts +++ b/packages/firestore/__tests__/pipelines.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it, jest } from '@jest/globals'; import { firebase } from '../lib'; import { arrayFilter, + arrayFirst, + arrayFirstN, arrayGet, and, conditional, @@ -205,6 +207,78 @@ describe('Firestore pipelines runtime', function () { }); }); + it('serializes arrayFirst and arrayFirstN as function expression helpers', function () { + const db: any = firebase.firestore(); + const serialized = db + .pipeline() + .collection('firestore') + .select( + arrayFirst('items').as('firstItem'), + arrayFirstN(field('items'), 2).as('firstTwoItems'), + arrayFirstN('items', field('count')).as('dynamicFirstItems'), + field('items').arrayFirst().as('fluentFirstItem'), + field('items').arrayFirstN(2).as('fluentFirstTwoItems'), + ) + .serialize(); + + expect(serialized.stages[0]).toMatchObject({ + stage: 'select', + options: { + selections: [ + { + alias: 'firstItem', + expr: { + exprType: 'Function', + name: 'arrayFirst', + args: [{ exprType: 'Field', path: 'items' }], + }, + }, + { + alias: 'firstTwoItems', + expr: { + exprType: 'Function', + name: 'arrayFirstN', + args: [ + { exprType: 'Field', path: 'items' }, + { exprType: 'Constant', value: 2 }, + ], + }, + }, + { + alias: 'dynamicFirstItems', + expr: { + exprType: 'Function', + name: 'arrayFirstN', + args: [ + { exprType: 'Field', path: 'items' }, + { exprType: 'Field', path: 'count' }, + ], + }, + }, + { + alias: 'fluentFirstItem', + expr: { + exprType: 'Function', + name: 'arrayFirst', + args: [{ exprType: 'Field', path: 'items' }], + }, + }, + { + alias: 'fluentFirstTwoItems', + expr: { + exprType: 'Function', + name: 'arrayFirstN', + args: [ + { exprType: 'Field', path: 'items' }, + { exprType: 'Constant', value: 2 }, + ], + }, + }, + ], + }, + }); + }); + it('enforces union guards and self-cycle serialization constraints', function () { const db: any = firebase.firestore(); const secondaryDb: any = firebase.app('secondaryFromNative').firestore(); diff --git a/packages/firestore/consumer-type-test.ts b/packages/firestore/consumer-type-test.ts index 78e17604d0..010fed6862 100644 --- a/packages/firestore/consumer-type-test.ts +++ b/packages/firestore/consumer-type-test.ts @@ -172,6 +172,8 @@ import { // array array, arrayFilter, + arrayFirst, + arrayFirstN, arrayConcat, arrayGet, arrayLength, @@ -1318,6 +1320,17 @@ void variable('score'); void arrayFilter('scores', 'score', greaterThan(variable('score'), constant(15))); void arrayFilter(field('scores'), 'score', greaterThan(variable('score'), constant(15))); void field('scores').arrayFilter('score', greaterThan(variable('score'), constant(15))); +// arrayFirst: (string) | (Expression) +void arrayFirst('items'); +void arrayFirst(field('items')); +// arrayFirstN: 4 overloads +void arrayFirstN('items', 2); +void arrayFirstN('items', field('limit')); +void arrayFirstN(field('items'), 2); +void arrayFirstN(field('items'), field('limit')); +void field('items').arrayFirst(); +void field('items').arrayFirstN(2); +void field('items').arrayFirstN(field('limit')); // arrayConcat: (Expression, ...) | (string, ...) void arrayConcat(field('tags'), field('moreTags')); void arrayConcat(field('tags'), ['extra']); @@ -1692,6 +1705,10 @@ const pipelineArrayOps = xDb array([constant(1), constant(2), constant(3)]).as('fixedArr'), arrayLength(field('comments')).as('commentCount'), arrayLength('comments').as('commentCount2'), + arrayFirst(field('items')).as('firstItemByHelper'), + arrayFirst('items').as('firstItemByField'), + arrayFirstN(field('items'), 2).as('firstItems'), + arrayFirstN('items', field('limit')).as('dynamicFirstItems'), arrayGet(field('items'), 0).as('firstItem'), arrayGet(field('items'), field('idx')).as('dynamicItem'), arrayGet('items', 0).as('firstItem2'), diff --git a/packages/firestore/e2e/Pipeline.e2e.js b/packages/firestore/e2e/Pipeline.e2e.js index 106ac57d99..698e0da0fa 100644 --- a/packages/firestore/e2e/Pipeline.e2e.js +++ b/packages/firestore/e2e/Pipeline.e2e.js @@ -1510,13 +1510,15 @@ describe('FirestorePipeline', function () { }); describe('array operators', function () { - it('evaluates array, arrayLength, arrayGet, arrayConcat, arraySum and array predicates', async function () { + it('evaluates array helpers and array predicates', async function () { const { execute, field, constant, array, arrayLength, + arrayFirst, + arrayFirstN, arrayGet, arrayConcat, arrayFilter, @@ -1564,6 +1566,8 @@ describe('FirestorePipeline', function () { .select( array([constant(1), constant(2), constant(3)]).as('fixedArr'), arrayLength(field('tags')).as('tagCount'), + arrayFirst(field('items')).as('firstItemByHelper'), + arrayFirstN(field('items'), 2).as('firstTwoItems'), arrayGet(field('items'), 0).as('firstItem'), arrayConcat(field('primaryTags'), field('secondaryTags')).as('allTags'), arrayFilter(field('scores'), 'score', greaterThan(variable('score'), 15)).as( @@ -1589,6 +1593,8 @@ describe('FirestorePipeline', function () { .select( array([constant(1), constant(2), constant(3)]).as('fixedArr'), arrayLength(field('tags')).as('tagCount'), + arrayFirst(field('items')).as('firstItemByHelper'), + arrayFirstN('items', 2).as('firstTwoItems'), arrayConcat(field('primaryTags'), field('secondaryTags')).as('allTags'), arrayFilter('scores', 'score', greaterThan(variable('score'), 15)).as( 'filteredItems', @@ -1601,6 +1607,8 @@ describe('FirestorePipeline', function () { const iosData = iosSnapshot.results[0].data(); iosData.fixedArr.should.eql([1, 2, 3]); iosData.tagCount.should.equal(2); + iosData.firstItemByHelper.should.equal('x'); + iosData.firstTwoItems.should.eql(['x', 'y']); iosData.allTags.should.eql(['a', 'b', 'c', 'd']); iosData.filteredItems.should.eql([20, 30]); iosData.totalScore.should.equal(60); @@ -1613,6 +1621,8 @@ describe('FirestorePipeline', function () { const data = snapshot.results[0].data(); data.fixedArr.should.eql([1, 2, 3]); data.tagCount.should.equal(2); + data.firstItemByHelper.should.equal('x'); + data.firstTwoItems.should.eql(['x', 'y']); data.firstItem.should.equal('x'); data.allTags.should.eql(['a', 'b', 'c', 'd']); data.filteredItems.should.eql([20, 30]); diff --git a/packages/firestore/type-test.ts b/packages/firestore/type-test.ts index f459dd0903..5e255682b6 100644 --- a/packages/firestore/type-test.ts +++ b/packages/firestore/type-test.ts @@ -171,6 +171,8 @@ import { // array array, arrayFilter, + arrayFirst, + arrayFirstN, arrayConcat, arrayGet, arrayLength, @@ -1320,6 +1322,17 @@ void variable('score'); void arrayFilter('scores', 'score', greaterThan(variable('score'), constant(15))); void arrayFilter(field('scores'), 'score', greaterThan(variable('score'), constant(15))); void field('scores').arrayFilter('score', greaterThan(variable('score'), constant(15))); +// arrayFirst: (string) | (Expression) +void arrayFirst('items'); +void arrayFirst(field('items')); +// arrayFirstN: 4 overloads +void arrayFirstN('items', 2); +void arrayFirstN('items', field('limit')); +void arrayFirstN(field('items'), 2); +void arrayFirstN(field('items'), field('limit')); +void field('items').arrayFirst(); +void field('items').arrayFirstN(2); +void field('items').arrayFirstN(field('limit')); // arrayConcat: (Expression, ...) | (string, ...) void arrayConcat(field('tags'), field('moreTags')); void arrayConcat(field('tags'), ['extra']); @@ -1694,6 +1707,10 @@ const pipelineArrayOps = xDb array([constant(1), constant(2), constant(3)]).as('fixedArr'), arrayLength(field('comments')).as('commentCount'), arrayLength('comments').as('commentCount2'), + arrayFirst(field('items')).as('firstItemByHelper'), + arrayFirst('items').as('firstItemByField'), + arrayFirstN(field('items'), 2).as('firstItems'), + arrayFirstN('items', field('limit')).as('dynamicFirstItems'), arrayGet(field('items'), 0).as('firstItem'), arrayGet(field('items'), field('idx')).as('dynamicItem'), arrayGet('items', 0).as('firstItem2'), From 04f116daeec60719c30d90ad37a16f04cb9febe5 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 12 May 2026 14:43:58 +0100 Subject: [PATCH 5/6] chore(firestore): arrayFirst and arrayFirstN API removed from config --- .../scripts/compare-types/configs/firestore-pipelines.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/scripts/compare-types/configs/firestore-pipelines.ts b/.github/scripts/compare-types/configs/firestore-pipelines.ts index fe5185b56a..28ff9fe5a2 100644 --- a/.github/scripts/compare-types/configs/firestore-pipelines.ts +++ b/.github/scripts/compare-types/configs/firestore-pipelines.ts @@ -23,14 +23,6 @@ import type { PackageConfig } from '../src/types'; const config: PackageConfig = { nameMapping: {}, missingInRN: [ - { - name: 'arrayFirst', - reason: 'Newer firebase-js-sdk array expression helper not yet exposed by RN Firebase pipelines.', - }, - { - name: 'arrayFirstN', - reason: 'Newer firebase-js-sdk array expression helper not yet exposed by RN Firebase pipelines.', - }, { name: 'arrayIndexOf', reason: 'Newer firebase-js-sdk array expression helper not yet exposed by RN Firebase pipelines.', From 05461eb86797c99686074902648dcb87a781a05c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 12 May 2026 17:18:11 +0100 Subject: [PATCH 6/6] chore: iOS does not support arrayFirst and arrayFirstN yet --- packages/firestore/__tests__/pipelines.test.ts | 6 ++++++ packages/firestore/e2e/Pipeline.e2e.js | 10 +++++----- packages/firestore/lib/pipelines/pipeline_support.ts | 2 ++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/firestore/__tests__/pipelines.test.ts b/packages/firestore/__tests__/pipelines.test.ts index 09623b5765..3980744fa1 100644 --- a/packages/firestore/__tests__/pipelines.test.ts +++ b/packages/firestore/__tests__/pipelines.test.ts @@ -526,6 +526,10 @@ describe('Firestore pipelines runtime', function () { .pipeline() .documents(['firestore/a']) .select( + arrayFirst(field('items')).as('firstArrayItem'), + arrayFirstN(field('items'), 2).as('firstArrayItems'), + field('items').arrayFirst().as('fluentFirstArrayItem'), + field('items').arrayFirstN(2).as('fluentFirstArrayItems'), arrayGet(field('items'), 0).as('firstItem'), conditional( field('value').greaterThan(0), @@ -542,6 +546,8 @@ describe('Firestore pipelines runtime', function () { .serialize(); expect(getIOSUnsupportedPipelineFunctions(serialized)).toEqual([ + 'arrayFirst', + 'arrayFirstN', 'arrayGet', 'conditional', 'round', diff --git a/packages/firestore/e2e/Pipeline.e2e.js b/packages/firestore/e2e/Pipeline.e2e.js index 698e0da0fa..bb51500627 100644 --- a/packages/firestore/e2e/Pipeline.e2e.js +++ b/packages/firestore/e2e/Pipeline.e2e.js @@ -1577,7 +1577,11 @@ describe('FirestorePipeline', function () { ); if (Platform.ios) { - await expectIOSUnsupportedFunctions(() => execute(pipeline), ['arrayGet']); + await expectIOSUnsupportedFunctions(() => execute(pipeline), [ + 'arrayFirst', + 'arrayFirstN', + 'arrayGet', + ]); const iosSnapshot = await execute( db @@ -1593,8 +1597,6 @@ describe('FirestorePipeline', function () { .select( array([constant(1), constant(2), constant(3)]).as('fixedArr'), arrayLength(field('tags')).as('tagCount'), - arrayFirst(field('items')).as('firstItemByHelper'), - arrayFirstN('items', 2).as('firstTwoItems'), arrayConcat(field('primaryTags'), field('secondaryTags')).as('allTags'), arrayFilter('scores', 'score', greaterThan(variable('score'), 15)).as( 'filteredItems', @@ -1607,8 +1609,6 @@ describe('FirestorePipeline', function () { const iosData = iosSnapshot.results[0].data(); iosData.fixedArr.should.eql([1, 2, 3]); iosData.tagCount.should.equal(2); - iosData.firstItemByHelper.should.equal('x'); - iosData.firstTwoItems.should.eql(['x', 'y']); iosData.allTags.should.eql(['a', 'b', 'c', 'd']); iosData.filteredItems.should.eql([20, 30]); iosData.totalScore.should.equal(60); diff --git a/packages/firestore/lib/pipelines/pipeline_support.ts b/packages/firestore/lib/pipelines/pipeline_support.ts index 06e13abb0a..8b4a5afa2b 100644 --- a/packages/firestore/lib/pipelines/pipeline_support.ts +++ b/packages/firestore/lib/pipelines/pipeline_support.ts @@ -53,6 +53,8 @@ export const PIPELINE_UNSUPPORTED_BASE_MESSAGE = // `RNFBFirestorePipelineNodeBuilder.swift`. // Remove entries once the iOS node builder/runtime path supports them. const IOS_UNSUPPORTED_FUNCTION_NAMES = new Set([ + 'arrayFirst', + 'arrayFirstN', 'arrayGet', 'conditional', 'round',