Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions .github/scripts/compare-types/configs/firestore-pipelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ import type { PackageConfig } from '../src/types';
const config: PackageConfig = {
nameMapping: {},
missingInRN: [
{
name: 'arrayFilter',
reason: 'Newer firebase-js-sdk array expression helper not yet exposed by RN Firebase pipelines.',
},
{
name: 'arrayFirst',
reason: 'Newer firebase-js-sdk array expression helper not yet exposed by RN Firebase pipelines.',
Expand Down Expand Up @@ -127,10 +123,6 @@ const config: PackageConfig = {
name: 'timestampExtract',
reason: 'Newer firebase-js-sdk timestamp expression helper not yet exposed by RN Firebase pipelines.',
},
{
name: 'variable',
reason: 'Newer firebase-js-sdk variable expression helper not yet exposed by RN Firebase pipelines.',
},
{
name: 'DefineStageOptions',
reason: 'Newer firebase-js-sdk stage options type not yet exposed by RN Firebase pipelines.',
Expand Down Expand Up @@ -169,7 +161,7 @@ const config: PackageConfig = {
},
{
name: 'ExpressionType',
reason: 'RN Firebase has not yet exposed the newer firebase-js-sdk `Variable` and `PipelineValue` expression kinds.',
reason: 'RN Firebase has not yet exposed the newer firebase-js-sdk `PipelineValue` expression kind.',
},
{
name: 'StageOptions',
Expand Down
4 changes: 3 additions & 1 deletion packages/firestore/__tests__/pipelines-web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jest.mock('@react-native-firebase/app/dist/module/internal/web/firebaseFirestore
conditional: jest.fn(actual.conditional as (...args: unknown[]) => unknown),
isType: jest.fn(actual.isType as (...args: unknown[]) => unknown),
mapGet: jest.fn(actual.mapGet as (...args: unknown[]) => unknown),
variable: jest.fn(actual.variable as (...args: unknown[]) => unknown),
euclideanDistance: jest.fn(actual.euclideanDistance as (...args: unknown[]) => unknown),
};
});
Expand Down Expand Up @@ -70,7 +71,7 @@ describe('Firestore web pipeline bridge', function () {
exprType: 'Function',
name: 'greaterThan',
args: [
{ __kind: 'expression', exprType: 'Field', path: 'rating' },
{ __kind: 'expression', exprType: 'Variable', name: 'score' },
{ __kind: 'expression', exprType: 'Constant', value: 3 },
],
},
Expand Down Expand Up @@ -124,6 +125,7 @@ describe('Firestore web pipeline bridge', function () {
const whereArg = (pipelineInstance.where as jest.Mock).mock.calls[0][0] as any;
expect(whereArg).toBeDefined();
expect(whereArg.__kind).toBeUndefined();
expect(firebaseFirestorePipelines.variable).toHaveBeenCalledWith('score');

const selectArg = (pipelineInstance.select as jest.Mock).mock.calls[0][0] as any;
expect(selectArg).toBeDefined();
Expand Down
64 changes: 64 additions & 0 deletions packages/firestore/__tests__/pipelines.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it, jest } from '@jest/globals';
import { firebase } from '../lib';
import {
arrayFilter,
arrayGet,
and,
conditional,
Expand All @@ -16,6 +17,7 @@ import {
timestampAdd,
timestampSubtract,
trunc,
variable,
} from '../lib/pipelines';
import '../lib/pipelines';
import { ConstantExpression } from '../lib/pipelines/expressions';
Expand Down Expand Up @@ -141,6 +143,68 @@ describe('Firestore pipelines runtime', function () {
});
});

it('serializes arrayFilter as a function expression helper and fluent method', function () {
const db: any = firebase.firestore();
const serialized = db
.pipeline()
.collection('firestore')
.select(
arrayFilter('scores', 'score', greaterThan(variable('score'), constant(15))).as(
'passingScores',
),
field('scores').arrayFilter('score', greaterThan(variable('score'), constant(20))).as(
'topScores',
),
)
.serialize();

expect(serialized.stages[0]).toMatchObject({
stage: 'select',
options: {
selections: [
{
alias: 'passingScores',
expr: {
exprType: 'Function',
name: 'arrayFilter',
args: [
{ exprType: 'Field', path: 'scores' },
{ exprType: 'Constant', value: 'score' },
{
exprType: 'Function',
name: 'greaterThan',
args: [
{ exprType: 'Variable', name: 'score' },
{ exprType: 'Constant', value: 15 },
],
},
],
},
},
{
alias: 'topScores',
expr: {
exprType: 'Function',
name: 'arrayFilter',
args: [
{ exprType: 'Field', path: 'scores' },
{ exprType: 'Constant', value: 'score' },
{
exprType: 'Function',
name: 'greaterThan',
args: [
{ exprType: 'Variable', name: 'score' },
{ exprType: 'Constant', value: 20 },
],
},
],
},
},
],
},
});
});

it('enforces union guards and self-cycle serialization constraints', function () {
const db: any = firebase.firestore();
const secondaryDb: any = firebase.app('secondaryFromNative').firestore();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,22 @@ private void processObjectLoweringStack(ArrayDeque<ObjectLoweringFrame> stack)
break;
}

Object exprType = map.get("exprType");
if (exprType instanceof String
&& "variable".equals(((String) exprType).toLowerCase(Locale.ROOT))) {
Object nameValue = map.get("name");
if (!(nameValue instanceof String) || ((String) nameValue).isEmpty()) {
throw new ReactNativeFirebaseFirestorePipelineExecutor.PipelineValidationException(
"pipelineExecute() expected "
+ currentFieldName
+ ".name to be a non-empty string.");
}
enterFrame.box.value =
applyPendingUnaryExpressionFunctions(
Expression.variable((String) nameValue), pendingUnaryFunctions);
break;
}

Object name = map.get("name");
if (name instanceof String) {
String functionName = (String) name;
Expand Down Expand Up @@ -1022,7 +1038,7 @@ private void processObjectLoweringStack(ArrayDeque<ObjectLoweringFrame> stack)
break;
}

Object exprType = map.get("exprType");
exprType = map.get("exprType");
if (exprType instanceof String) {
String normalizedType = ((String) exprType).toLowerCase(Locale.ROOT);
if ("field".equals(normalizedType)) {
Expand Down Expand Up @@ -3287,6 +3303,19 @@ private Object serializeExpressionNode(
continue;
}

if (expression
instanceof ReactNativeFirebaseFirestorePipelineParser.ParsedVariableExpressionNode) {
Map<String, Object> output = new LinkedHashMap<>();
output.put("__kind", "expression");
output.put("exprType", "Variable");
output.put(
"name",
((ReactNativeFirebaseFirestorePipelineParser.ParsedVariableExpressionNode) expression)
.name);
enterFrame.box.value = output;
continue;
}

ReactNativeFirebaseFirestorePipelineParser.ParsedFunctionExpressionNode function =
(ReactNativeFirebaseFirestorePipelineParser.ParsedFunctionExpressionNode) expression;
List<SerializedValueBox> argBoxes = new ArrayList<>(function.args.size());
Expand Down Expand Up @@ -3532,6 +3561,19 @@ private Object serializeValueNode(
continue;
}

if (expression
instanceof ReactNativeFirebaseFirestorePipelineParser.ParsedVariableExpressionNode) {
Map<String, Object> output = new LinkedHashMap<>();
output.put("__kind", "expression");
output.put("exprType", "Variable");
output.put(
"name",
((ReactNativeFirebaseFirestorePipelineParser.ParsedVariableExpressionNode) expression)
.name);
enterFrame.box.value = output;
continue;
}

ReactNativeFirebaseFirestorePipelineParser.ParsedFunctionExpressionNode function =
(ReactNativeFirebaseFirestorePipelineParser.ParsedFunctionExpressionNode) expression;
List<SerializedValueBox> argBoxes = new ArrayList<>(function.args.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,15 @@ private static void parseExpressionValueTree(ExpressionValueParseFrame initialFr
stack.push(new ValueEnterFrame(map.get("value"), valueBox, fieldName + ".value"));
continue;
}
if ("variable".equals(normalizedType)) {
Object nameValue = map.get("name");
if (!(nameValue instanceof String) || ((String) nameValue).isEmpty()) {
throw new ReactNativeFirebaseFirestorePipelineExecutor.PipelineValidationException(
"pipelineExecute() expected " + fieldName + ".name to be a non-empty string.");
}
enterFrame.box.value = new ParsedVariableExpressionNode((String) nameValue);
continue;
}
}

if (map.containsKey("name")) {
Expand Down Expand Up @@ -1636,6 +1645,14 @@ static final class ParsedConstantExpressionNode extends ParsedExpressionNode {
}
}

static final class ParsedVariableExpressionNode extends ParsedExpressionNode {
final String name;

ParsedVariableExpressionNode(String name) {
this.name = name;
}
}

static final class ParsedFunctionExpressionNode extends ParsedExpressionNode {
final String name;
final List<ParsedValueNode> args;
Expand Down
14 changes: 14 additions & 0 deletions packages/firestore/consumer-type-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,10 @@ import {
collectionId,
type as pipelineType,
currentTimestamp,
variable,
// array
array,
arrayFilter,
arrayConcat,
arrayGet,
arrayLength,
Expand Down Expand Up @@ -1310,6 +1312,12 @@ void currentTimestamp();
// array
void array([1, 2, 3]);
void array([field('a'), constant(2)]);
// variable: (string) => Expression
void variable('score');
// arrayFilter: (string, alias, BooleanExpression) | (Expression, alias, BooleanExpression)
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)));
// arrayConcat: (Expression, ...) | (string, ...)
void arrayConcat(field('tags'), field('moreTags'));
void arrayConcat(field('tags'), ['extra']);
Expand Down Expand Up @@ -1689,6 +1697,12 @@ const pipelineArrayOps = xDb
arrayGet('items', 0).as('firstItem2'),
arrayConcat(field('primaryTags'), field('secondaryTags')).as('allTags'),
arrayConcat('primaryTags', ['extra']).as('allTags2'),
arrayFilter('scores', 'score', greaterThan(variable('score'), constant(15))).as(
'passingScores',
),
field('scores').arrayFilter('score', greaterThan(variable('score'), constant(20))).as(
'topScores',
),
arraySum(field('scores')).as('totalScore'),
arraySum('scores').as('totalScore2'),
);
Expand Down
11 changes: 11 additions & 0 deletions packages/firestore/e2e/Pipeline.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -1519,8 +1519,11 @@ describe('FirestorePipeline', function () {
arrayLength,
arrayGet,
arrayConcat,
arrayFilter,
arraySum,
variable,
and,
greaterThan,
arrayContains,
arrayContainsAny,
arrayContainsAll,
Expand Down Expand Up @@ -1563,6 +1566,9 @@ describe('FirestorePipeline', function () {
arrayLength(field('tags')).as('tagCount'),
arrayGet(field('items'), 0).as('firstItem'),
arrayConcat(field('primaryTags'), field('secondaryTags')).as('allTags'),
arrayFilter(field('scores'), 'score', greaterThan(variable('score'), 15)).as(
'filteredItems',
),
arraySum(field('scores')).as('totalScore'),
);

Expand All @@ -1584,6 +1590,9 @@ describe('FirestorePipeline', function () {
array([constant(1), constant(2), constant(3)]).as('fixedArr'),
arrayLength(field('tags')).as('tagCount'),
arrayConcat(field('primaryTags'), field('secondaryTags')).as('allTags'),
arrayFilter('scores', 'score', greaterThan(variable('score'), 15)).as(
'filteredItems',
),
arraySum(field('scores')).as('totalScore'),
),
);
Expand All @@ -1593,6 +1602,7 @@ describe('FirestorePipeline', function () {
iosData.fixedArr.should.eql([1, 2, 3]);
iosData.tagCount.should.equal(2);
iosData.allTags.should.eql(['a', 'b', 'c', 'd']);
iosData.filteredItems.should.eql([20, 30]);
iosData.totalScore.should.equal(60);
return;
}
Expand All @@ -1605,6 +1615,7 @@ describe('FirestorePipeline', function () {
data.tagCount.should.equal(2);
data.firstItem.should.equal('x');
data.allTags.should.eql(['a', 'b', 'c', 'd']);
data.filteredItems.should.eql([20, 30]);
data.totalScore.should.equal(60);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,12 @@ final class RNFBFirestorePipelineBridgeFactory {
let childBox = QuerySourceValueBox()
stack.append(.expressionConstantExit(box, childBox))
stack.append(.value(value, childBox))
case let .variable(name):
box.value = [
"__kind": "expression",
"exprType": "Variable",
"name": name,
]
case let .function(name, args):
let childBoxes = args.map { _ in QuerySourceValueBox() }
stack.append(.expressionFunctionExit(name, box, childBoxes))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,14 @@ final class RNFBFirestorePipelineNodeBuilder {
break expressionLoop
}

if let kind = (map["exprType"] as? String)?.lowercased(), kind == "variable" {
guard let name = map["name"] as? String, !name.isEmpty else {
throw PipelineValidationError("pipelineExecute() expected \(currentField).name to be a non-empty string.")
}
box.value = VariableBridge(name: name)
break expressionLoop
}

if let name = map["name"] as? String {
let rawArgs: [Any]
if let args = map["args"] as? [Any] {
Expand Down Expand Up @@ -1237,6 +1245,14 @@ final class RNFBFirestorePipelineNodeBuilder {
break expressionLoop
}

if let kind = (map["exprType"] as? String)?.lowercased(), kind == "variable" {
guard let name = map["name"] as? String, !name.isEmpty else {
throw PipelineValidationError("pipelineExecute() expected \(currentField).name to be a non-empty string.")
}
box.value = VariableBridge(name: name)
break expressionLoop
}

throw PipelineValidationError(
"pipelineExecute() could not convert \(currentField) into a pipeline expression.")
}
Expand Down Expand Up @@ -1357,6 +1373,12 @@ final class RNFBFirestorePipelineNodeBuilder {
let valueBox = SerializedValueBox()
stack.append(.expressionConstantExit(box, valueBox))
stack.append(.valueEnter(constantValue, valueBox))
case let .variable(name):
box.value = [
"__kind": "expression",
"exprType": "Variable",
"name": name,
]
case let .function(name, args):
let argBoxes = args.map { _ in SerializedValueBox() }
stack.append(.expressionFunctionExit(box, name, argBoxes))
Expand Down Expand Up @@ -1463,6 +1485,12 @@ final class RNFBFirestorePipelineNodeBuilder {
let valueBox = SerializedValueBox()
stack.append(.expressionConstantExit(box, valueBox))
stack.append(.valueEnter(constantValue, valueBox))
case let .variable(name):
box.value = [
"__kind": "expression",
"exprType": "Variable",
"name": name,
]
case let .function(name, args):
let argBoxes = args.map { _ in SerializedValueBox() }
stack.append(.expressionFunctionExit(box, name, argBoxes))
Expand Down
Loading
Loading