diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java index be2e24be..e11a233e 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -68,6 +68,10 @@ class FirestoreSerialize { private static final String TYPE_FIELDVALUE = "fieldvalue"; private static final String TYPE_FIELDVALUE_DELETE = "delete"; private static final String TYPE_FIELDVALUE_TIMESTAMP = "timestamp"; + private static final String TYPE_FIELDVALUE_UNION = "union"; + private static final String TYPE_FIELDVALUE_REMOVE = "remove"; + private static final String TYPE_FIELDVALUE_TYPE = "type"; + private static final String TYPE_FIELDVALUE_ELEMENTS = "elements"; // Document Change Types private static final String CHANGE_ADDED = "added"; @@ -446,7 +450,9 @@ class FirestoreSerialize { } if (TYPE_FIELDVALUE.equals(type)) { - String fieldValueType = typeMap.getString(VALUE); + ReadableMap fieldValueMap = typeMap.getMap(VALUE); + String fieldValueType = fieldValueMap.getString(TYPE_FIELDVALUE_TYPE); + if (TYPE_FIELDVALUE_TIMESTAMP.equals(fieldValueType)) { return FieldValue.serverTimestamp(); @@ -456,6 +462,16 @@ class FirestoreSerialize { return FieldValue.delete(); } + if (TYPE_FIELDVALUE_UNION.equals(fieldValueType)) { + ReadableArray elements = fieldValueMap.getArray(TYPE_FIELDVALUE_ELEMENTS); + return FieldValue.arrayUnion(elements.toArrayList().toArray()); + } + + if (TYPE_FIELDVALUE_REMOVE.equals(fieldValueType)) { + ReadableArray elements = fieldValueMap.getArray(TYPE_FIELDVALUE_ELEMENTS); + return FieldValue.arrayRemove(elements.toArrayList().toArray()); + } + Log.w(TAG, "Unknown FieldValue type: " + fieldValueType); return null; } diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m index 7e6457b0..32cd0bb6 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -37,6 +37,10 @@ static NSString *const typeTimestamp = @"timestamp"; static NSString *const typeReference = @"reference"; static NSString *const typeDocumentId = @"documentid"; static NSString *const typeFieldValue = @"fieldvalue"; +static NSString *const typeFieldValueUnion = @"union"; +static NSString *const typeFieldValueRemove = @"remove"; +static NSString *const typeFieldValueType = @"type"; +static NSString *const typeFieldValueElements = @"elements"; - (id)initWithPath:(RCTEventEmitter *)emitter appDisplayName:(NSString *)appDisplayName @@ -408,8 +412,9 @@ static NSString *const typeFieldValue = @"fieldvalue"; } if ([type isEqualToString:typeFieldValue]) { - NSString *string = (NSString *) value; - + NSDictionary *fieldValueMap = (NSDictionary *) value; + NSString *string = (NSString *) fieldValueMap[typeFieldValueType]; + if ([string isEqualToString:typeDelete]) { return [FIRFieldValue fieldValueForDelete]; } @@ -417,6 +422,16 @@ static NSString *const typeFieldValue = @"fieldvalue"; if ([string isEqualToString:typeTimestamp]) { return [FIRFieldValue fieldValueForServerTimestamp]; } + + if ([string isEqualToString:typeFieldValueUnion]) { + NSDictionary *elements = (NSDictionary *) value[typeFieldValueElements]; + return [FIRFieldValue fieldValueForArrayUnion:elements]; + } + + if ([string isEqualToString:typeFieldValueRemove]) { + NSDictionary *elements = (NSDictionary *) value[typeFieldValueElements]; + return [FIRFieldValue fieldValueForArrayRemove:elements]; + } DLog(@"RNFirebaseFirestore: Unsupported field-value sent to parseJSTypeMap - value is %@", NSStringFromClass([value class])); diff --git a/src/index.d.ts b/src/index.d.ts index eee0f45d..6b50acce 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2404,10 +2404,16 @@ declare module 'react-native-firebase' { constructor(...segments: string[]); } + type AnyJs = null | undefined | boolean | number | string | object; + class FieldValue { static delete(): FieldValue; static serverTimestamp(): FieldValue; + + static arrayUnion(...elements: AnyJs[]): FieldValue; + + static arrayRemove(...elements: AnyJs[]): FieldValue; } class GeoPoint { diff --git a/src/modules/firestore/FieldValue.js b/src/modules/firestore/FieldValue.js index 14067038..35132bf1 100644 --- a/src/modules/firestore/FieldValue.js +++ b/src/modules/firestore/FieldValue.js @@ -2,16 +2,45 @@ * @flow * FieldValue representation wrapper */ +import AnyJs from './utils/any'; +// TODO: Salakar: Refactor in v6 export default class FieldValue { + _type: string; + + _elements: AnyJs[] | any; + + constructor(type: string, elements?: AnyJs[]) { + this._type = type; + this._elements = elements; + } + + get type(): string { + return this._type; + } + + get elements(): AnyJs[] { + return this._elements; + } + static delete(): FieldValue { - return DELETE_FIELD_VALUE; + return new FieldValue(TypeFieldValueDelete); } static serverTimestamp(): FieldValue { - return SERVER_TIMESTAMP_FIELD_VALUE; + return new FieldValue(TypeFieldValueTimestamp); + } + + static arrayUnion(...elements: AnyJs[]) { + return new FieldValue(TypeFieldValueUnion, elements); + } + + static arrayRemove(...elements: AnyJs[]) { + return new FieldValue(TypeFieldValueRemove, elements); } } -export const DELETE_FIELD_VALUE = new FieldValue(); -export const SERVER_TIMESTAMP_FIELD_VALUE = new FieldValue(); +export const TypeFieldValueDelete = 'delete'; +export const TypeFieldValueRemove = 'remove'; +export const TypeFieldValueUnion = 'union'; +export const TypeFieldValueTimestamp = 'timestamp'; diff --git a/src/modules/firestore/utils/any.js b/src/modules/firestore/utils/any.js new file mode 100644 index 00000000..af9d484d --- /dev/null +++ b/src/modules/firestore/utils/any.js @@ -0,0 +1,4 @@ +/** + * @url https://github.com/firebase/firebase-js-sdk/blob/master/packages/firestore/src/util/misc.ts#L26 + */ +export type AnyJs = null | undefined | boolean | number | string | object; diff --git a/src/modules/firestore/utils/serialize.js b/src/modules/firestore/utils/serialize.js index 8f1fb3b8..77e9fc5a 100644 --- a/src/modules/firestore/utils/serialize.js +++ b/src/modules/firestore/utils/serialize.js @@ -5,10 +5,7 @@ import DocumentReference from '../DocumentReference'; import Blob from '../Blob'; import { DOCUMENT_ID } from '../FieldPath'; -import { - DELETE_FIELD_VALUE, - SERVER_TIMESTAMP_FIELD_VALUE, -} from '../FieldValue'; +import FieldValue from '../FieldValue'; import GeoPoint from '../GeoPoint'; import Path from '../Path'; import { typeOf } from '../../../utils'; @@ -72,20 +69,6 @@ export const buildTypeMap = (value: any): NativeTypeMap | null => { }; } - if (value === DELETE_FIELD_VALUE) { - return { - type: 'fieldvalue', - value: 'delete', - }; - } - - if (value === SERVER_TIMESTAMP_FIELD_VALUE) { - return { - type: 'fieldvalue', - value: 'timestamp', - }; - } - if (value === DOCUMENT_ID) { return { type: 'documentid', @@ -139,6 +122,17 @@ export const buildTypeMap = (value: any): NativeTypeMap | null => { }; } + // TODO: Salakar: Refactor in v6 - add internal `type` flag + if (value instanceof FieldValue) { + return { + type: 'fieldvalue', + value: { + elements: value.elements, + type: value.type, + }, + }; + } + return { type: 'object', value: buildNativeMap(value), diff --git a/tests/e2e/firestore/fieldValue.e2e.js b/tests/e2e/firestore/fieldValue.e2e.js index b70176bf..f4fe988e 100644 --- a/tests/e2e/firestore/fieldValue.e2e.js +++ b/tests/e2e/firestore/fieldValue.e2e.js @@ -7,7 +7,7 @@ const { describe('firestore()', () => { describe('FieldValue', () => { - before(async () => { + beforeEach(async () => { await resetTestCollectionDoc(DOC_2_PATH, DOC_2); }); @@ -46,5 +46,42 @@ describe('firestore()', () => { ); }); }); + describe('arrayUnion()', () => { + it('should add new values to array field', async () => { + const { data } = await testCollectionDoc(DOC_2_PATH).get(); + should.equal(data().elements, undefined); + + await testCollectionDoc(DOC_2_PATH).update({ + elements: firebase.firestore.FieldValue.arrayUnion('element 1'), + elements2: firebase.firestore.FieldValue.arrayUnion('element 2'), + }); + + const { data: dataAfterUpdate } = await testCollectionDoc( + DOC_2_PATH + ).get(); + + dataAfterUpdate().elements.should.containDeep(['element 1']); + dataAfterUpdate().elements2.should.containDeep(['element 2']); + }); + }); + describe('arrayRemove()', () => { + it('should remove value from array', async () => { + await testCollectionDoc(DOC_2_PATH).set({ + elements: ['element 1', 'element 2'], + }); + const { data } = await testCollectionDoc(DOC_2_PATH).get(); + data().elements.should.containDeep(['element 1', 'element 2']); + + await testCollectionDoc(DOC_2_PATH).update({ + elements: firebase.firestore.FieldValue.arrayRemove('element 2'), + }); + + const { data: dataAfterUpdate } = await testCollectionDoc( + DOC_2_PATH + ).get(); + + dataAfterUpdate().elements.should.not.containDeep(['element 2']); + }); + }); }); });