Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
35 changes: 35 additions & 0 deletions packages/firestore/__tests__/vector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-env jest */
import { describe, it, expect } from '@jest/globals';

describe('firestore() VectorValue', function () {
const { default: FirestoreVectorValue } = require('../lib/FirestoreVectorValue');
const serialize = require('../lib/utils/serialize');
const { getTypeMapName } = require('../lib/utils/typemap');

it('constructs and validates values', function () {
const v = new FirestoreVectorValue([0, 1.5, -2]);
expect(v.values).toEqual([0, 1.5, -2]);
expect(v.isEqual(new FirestoreVectorValue([0, 1.5, -2]))).toBe(true);
expect(v.isEqual(new FirestoreVectorValue([0, 1.5]))).toBe(false);
});

it('serializes to type map and parses back', function () {
const v = new FirestoreVectorValue([0.1, 0.2, 0.3]);
const typed = serialize.generateNativeData(v, false);
// [INT_VECTOR, [0.1,0.2,0.3]]
expect(Array.isArray(typed)).toBe(true);
expect(getTypeMapName(typed[0])).toBe('vector');
const parsed = serialize.parseNativeData(null, typed);
expect(parsed instanceof FirestoreVectorValue).toBe(true);
expect(parsed.values).toEqual([0.1, 0.2, 0.3]);
});

it('serializes inside objects and arrays', function () {
const v = new FirestoreVectorValue([1, 2, 3]);
const map = serialize.buildNativeMap({ a: v }, false);
expect(getTypeMapName(map.a[0])).toBe('vector');

const arr = serialize.buildNativeArray([v], false);
expect(getTypeMapName(arr[0][0])).toBe('vector');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public class ReactNativeFirebaseFirestoreSerialize {
private static final int INT_OBJECT = 16;
private static final int INT_INTEGER = 17;
private static final int INT_NEGATIVE_ZERO = 18;
private static final int INT_VECTOR = 19;
private static final int INT_UNKNOWN = -999;

// Keys
Expand Down Expand Up @@ -404,6 +405,36 @@ private static WritableArray buildTypeMap(Object value) {
return typeArray;
}

// VectorValue – detect via reflection to avoid compile-time dependency on newer SDKs
try {
Class<?> vectorClass = Class.forName("com.google.firebase.firestore.VectorValue");
if (vectorClass.isInstance(value)) {
typeArray.pushInt(INT_VECTOR);
WritableArray valuesArray = Arguments.createArray();
try {
double[] doubles = (double[]) vectorClass.getMethod("getValues").invoke(value);
if (doubles != null) {
for (double d : doubles) valuesArray.pushDouble(d);
}
} catch (Exception ignored) {
try {
Object result = vectorClass.getMethod("values").invoke(value);
if (result instanceof double[]) {
for (double d : (double[]) result) valuesArray.pushDouble(d);
} else if (result instanceof List) {
for (Object o : (List<?>) result) valuesArray.pushDouble(((Number) o).doubleValue());
}
} catch (Exception ignored2) {
// leave empty if not accessible
}
}
typeArray.pushArray(valuesArray);
return typeArray;
}
} catch (ClassNotFoundException e) {
// Older SDK without VectorValue – fall through
}

Log.w(TAG, "Unknown object of type " + value.getClass());

typeArray.pushInt(INT_UNKNOWN);
Expand Down Expand Up @@ -520,6 +551,26 @@ static Object parseTypeMap(FirebaseFirestore firestore, ReadableArray typeArray)
}
case INT_OBJECT:
return parseReadableMap(firestore, typeArray.getMap(1));
case INT_VECTOR:
try {
Class<?> vectorClass = Class.forName("com.google.firebase.firestore.VectorValue");
ReadableArray vals = typeArray.getArray(1);
if (vals == null) return null;
double[] doubles = new double[vals.size()];
for (int i = 0; i < vals.size(); i++) doubles[i] = vals.getDouble(i);
try {
// Prefer static factory if available
return vectorClass.getMethod("from", double[].class).invoke(null, (Object) doubles);
} catch (Exception noFactory) {
try {
return vectorClass.getConstructor(double[].class).newInstance((Object) doubles);
} catch (Exception noCtor) {
return null;
}
}
} catch (ClassNotFoundException e) {
return null;
}
case INT_UNKNOWN:
default:
return null;
Expand Down
36 changes: 36 additions & 0 deletions packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ @implementation RNFBFirestoreSerialize
INT_OBJECT,
INT_INTEGER,
INT_NEGATIVE_ZERO,
INT_VECTOR,
INT_UNKNOWN = -999,
};

Expand Down Expand Up @@ -358,6 +359,24 @@ + (NSArray *)buildTypeMap:(id)value {
return typeArray;
}

// VectorValue (FIRVectorValue) – detect reflectively to avoid hard dependency on symbol
Class vectorClass = NSClassFromString(@"FIRVectorValue");
if (vectorClass != nil && [value isKindOfClass:vectorClass]) {
typeArray[0] = @(INT_VECTOR);
NSArray *values = nil;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([value respondsToSelector:@selector(values)]) {
values = [value performSelector:@selector(values)];
}
#pragma clang diagnostic pop
if (values == nil) {
values = @[];
}
typeArray[1] = values;
return typeArray;
}

typeArray[0] = @(INT_UNKNOWN);
return typeArray;
}
Expand Down Expand Up @@ -466,6 +485,23 @@ + (id)parseTypeMap:(FIRFirestore *)firestore typeMap:(NSArray *)typeMap {
}
case INT_OBJECT:
return [self parseNSDictionary:firestore dictionary:typeMap[1]];
case INT_VECTOR: {
NSArray *values = typeMap[1];
Class vectorClass = NSClassFromString(@"FIRVectorValue");
if (vectorClass != nil) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([vectorClass respondsToSelector:@selector(vectorWithValues:)]) {
return [vectorClass performSelector:@selector(vectorWithValues:) withObject:values];
}
id instance = [vectorClass alloc];
if ([instance respondsToSelector:@selector(initWithValues:)]) {
return [instance performSelector:@selector(initWithValues:) withObject:values];
}
#pragma clang diagnostic pop
}
return nil;
}
case INT_UNKNOWN:
default:
return nil;
Expand Down
5 changes: 5 additions & 0 deletions packages/firestore/lib/FirestoreStatics.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ import FirestoreFieldValue from './FirestoreFieldValue';
import FirestoreGeoPoint from './FirestoreGeoPoint';
import FirestoreTimestamp from './FirestoreTimestamp';
import { Filter } from './FirestoreFilter';
import FirestoreVectorValue from './FirestoreVectorValue';
export default {
Blob: FirestoreBlob,
FieldPath: FirestoreFieldPath,
FieldValue: createDeprecationProxy(FirestoreFieldValue),
GeoPoint: FirestoreGeoPoint,
Timestamp: createDeprecationProxy(FirestoreTimestamp),
Filter: createDeprecationProxy(Filter),
VectorValue: FirestoreVectorValue,
vector(values) {
return new FirestoreVectorValue(values);
},

CACHE_SIZE_UNLIMITED: -1,

Expand Down
74 changes: 74 additions & 0 deletions packages/firestore/lib/FirestoreVectorValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library 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 { isArray, isNumber } from '@react-native-firebase/app/lib/common';

export default class FirestoreVectorValue {
constructor(values) {
if (values === undefined) {
this._values = [];
return;
}

if (!isArray(values)) {
throw new Error(
"firebase.firestore.VectorValue(values?) 'values' expected an array of numbers or undefined.",
);
}

for (let i = 0; i < values.length; i++) {
const v = values[i];
if (!isNumber(v)) {
throw new Error(
`firebase.firestore.VectorValue(values?) 'values[${i}]' expected a number value.`,
);
}
}

// Store a shallow copy to ensure immutability semantics for the input array
this._values = values.slice();
}

get values() {
return this._values.slice();
}

isEqual(other) {
if (!(other instanceof FirestoreVectorValue)) {
throw new Error(
"firebase.firestore.VectorValue.isEqual(*) 'other' expected a VectorValue instance.",
);
}

const a = this._values;
const b = other._values;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
// Use strict equality; Firestore numbers allow NaN/Infinity – equality semantics match JS
if (a[i] !== b[i]) return false;
}
return true;
}

toJSON() {
return { values: this._values.slice() };
}

toString() {
return `FirestoreVectorValue(values=[${this._values.join(', ')}])`;
}
}
31 changes: 30 additions & 1 deletion packages/firestore/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ export namespace FirebaseFirestoreTypes {
| FieldPath
| FieldValue
| DocumentReference
| CollectionReference;
| CollectionReference
| VectorValue;

/**
* A `DocumentReference` refers to a document location in a Firestore database and can be used to write, read, or listen
Expand Down Expand Up @@ -2104,6 +2105,16 @@ export namespace FirebaseFirestoreTypes {
*/
Filter: typeof Filter;

/**
* Returns the `VectorValue` class.
*/
VectorValue: typeof VectorValue;

/**
* Creates a new VectorValue from the provided numbers.
*/
vector(values?: number[]): VectorValue;

/**
* Used to set the cache size to unlimited when passing to `cacheSizeBytes` in
* `firebase.firestore().settings()`.
Expand Down Expand Up @@ -2368,6 +2379,24 @@ export namespace FirebaseFirestoreTypes {
[P in keyof T]: SetValue<T[P]> | FieldValue; // allow FieldValue in place of values
}
: T;

/**
* An immutable object representing a vector in Firestore. The vector is a numeric array.
*/
export class VectorValue {
constructor(values?: number[]);
/** The numeric values of this VectorValue. */
readonly values: number[];
/** Returns true if this `VectorValue` is equal to the provided one. */
isEqual(other: VectorValue): boolean;
/** Returns a JSON-serializable representation of this VectorValue. */
toJSON(): { values: number[] };
}

/**
* Creates a new VectorValue from the provided numbers.
*/
export function vector(values?: number[]): VectorValue;
}

declare const defaultExport: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp<
Expand Down
13 changes: 13 additions & 0 deletions packages/firestore/lib/modular/VectorValue.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* A `VectorValue` represents a vector in Firestore. The vector is a numeric array.
* @param values - The numeric values of the vector.
* @returns A new VectorValue instance.
*/
export declare class VectorValue {
readonly values: number[];
constructor(values?: number[]);
isEqual(other: VectorValue): boolean;
toJSON(): { values: number[] };
}

export declare function vector(values?: number[]): VectorValue;
11 changes: 11 additions & 0 deletions packages/firestore/lib/modular/VectorValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import FirestoreVectorValue from '../FirestoreVectorValue';

export const VectorValue = FirestoreVectorValue;

/**
* @param {number[]=} values
* @returns {VectorValue}
*/
export function vector(values) {
return new VectorValue(values);
}
1 change: 1 addition & 0 deletions packages/firestore/lib/modular/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,3 +785,4 @@ export * from './FieldPath';
export * from './FieldValue';
export * from './GeoPoint';
export * from './Timestamp';
export * from './VectorValue';
1 change: 1 addition & 0 deletions packages/firestore/lib/modular/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,5 @@ export * from './FieldPath';
export * from './FieldValue';
export * from './GeoPoint';
export * from './Timestamp';
export * from './VectorValue';
export { Filter } from '../FirestoreFilter';
11 changes: 9 additions & 2 deletions packages/firestore/lib/utils/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import FirestorePath from '../FirestorePath';
import FirestoreTimestamp from '../FirestoreTimestamp';
import { getTypeMapInt, getTypeMapName } from './typemap';
import { Bytes } from '../modular/Bytes';
import FirestoreVectorValue from '../FirestoreVectorValue';

// To avoid React Native require cycle warnings
let FirestoreDocumentReference = null;
Expand Down Expand Up @@ -162,7 +163,7 @@ export function generateNativeData(value, ignoreUndefined) {
}

if (isObject(value)) {
if (value instanceof FirestoreDocumentReference) {
if (FirestoreDocumentReference && value instanceof FirestoreDocumentReference) {
return getTypeMapInt('reference', value.path);
}

Expand All @@ -185,10 +186,14 @@ export function generateNativeData(value, ignoreUndefined) {
return getTypeMapInt('blob', value.toBase64());
}

if (value instanceof FirestoreFieldValue) {
if (FirestoreFieldValue && value instanceof FirestoreFieldValue) {
return getTypeMapInt('fieldvalue', [value._type, value._elements]);
}

if (value instanceof FirestoreVectorValue) {
return getTypeMapInt('vector', value.values);
}

return getTypeMapInt('object', buildNativeMap(value, ignoreUndefined));
}

Expand Down Expand Up @@ -279,6 +284,8 @@ export function parseNativeData(firestore, nativeArray) {
return new FirestoreTimestamp(value[0], value[1]);
case 'blob':
return Bytes.fromBase64String(value);
case 'vector':
return new FirestoreVectorValue(value);
default:
// eslint-disable-next-line no-console
console.warn(`Unknown data type received from native channel: ${type}`);
Expand Down
1 change: 1 addition & 0 deletions packages/firestore/lib/utils/typemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const MAP = {
object: 16,
integer: 17,
negativeZero: 18,
vector: 19,
unknown: -999,
};

Expand Down
Loading