Skip to content
Draft
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
84 changes: 84 additions & 0 deletions packages/firestore/__tests__/firestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,90 @@ describe('Firestore', function () {
);
});

it('FirestoreDocumentSnapshot.data() respects SnapshotOptions.serverTimestamps', function () {
const firestore = getFirestore();
const snapshot = new FirestoreDocumentSnapshot(
// @ts-expect-error calling a private constructor directly which expects FirestoreInternal type
firestore,
{
data: { createdAt: [3] },
dataEstimate: { createdAt: [13, [123, 456]] },
dataPrevious: { createdAt: [13, [42, 0]] },
dataNone: { createdAt: [3] },
metadata: [false, true],
path: 'foo/bar',
exists: true,
},
null,
);

expect(snapshot.data()).toEqual({ createdAt: null });
expect(snapshot.data({ serverTimestamps: 'estimate' })?.createdAt).toBeInstanceOf(Timestamp);
expect(snapshot.data({ serverTimestamps: 'estimate' })?.createdAt).toMatchObject({
seconds: 123,
nanoseconds: 456,
});
expect(snapshot.data({ serverTimestamps: 'previous' })?.createdAt).toMatchObject({
seconds: 42,
nanoseconds: 0,
});
expect(snapshot.data({ serverTimestamps: 'none' })).toEqual({ createdAt: null });
});

it('FirestoreDocumentSnapshot.get() respects SnapshotOptions.serverTimestamps', function () {
const firestore = getFirestore();
const snapshot = new FirestoreDocumentSnapshot(
// @ts-expect-error calling a private constructor directly which expects FirestoreInternal type
firestore,
{
data: { nested: [16, { createdAt: [3] }] },
dataEstimate: { nested: [16, { createdAt: [13, [123, 456]] }] },
dataPrevious: { nested: [16, { createdAt: [13, [42, 0]] }] },
dataNone: { nested: [16, { createdAt: [3] }] },
metadata: [false, true],
path: 'foo/bar',
exists: true,
},
null,
);

expect(snapshot.get('nested.createdAt')).toBeNull();
expect(snapshot.get('nested.createdAt', { serverTimestamps: 'estimate' })).toMatchObject({
seconds: 123,
nanoseconds: 456,
});
expect(snapshot.get('nested.createdAt', { serverTimestamps: 'previous' })).toMatchObject({
seconds: 42,
nanoseconds: 0,
});
expect(snapshot.get('nested.createdAt', { serverTimestamps: 'none' })).toBeNull();
});

it('FirestoreDocumentSnapshot.data() passes SnapshotOptions through converter snapshots', function () {
const firestore = getFirestore();
const snapshot = new FirestoreDocumentSnapshot(
// @ts-expect-error calling a private constructor directly which expects FirestoreInternal type
firestore,
{
data: { createdAt: [3] },
dataEstimate: { createdAt: [13, [123, 456]] },
metadata: [false, true],
path: 'foo/bar',
exists: true,
},
{
toFirestore: data => data,
fromFirestore: converterSnapshot =>
converterSnapshot.data({ serverTimestamps: 'estimate' }),
},
);

expect(snapshot.data()?.createdAt).toMatchObject({
seconds: 123,
nanoseconds: 456,
});
});

describe('FieldValue', function () {
it('FieldValue.delete()', function () {
const fieldValue = firestore.FieldValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ public class ReactNativeFirebaseFirestoreSerialize {
// Keys
private static final String TYPE = "type";
private static final String KEY_DATA = "data";
private static final String KEY_DATA_ESTIMATE = "dataEstimate";
private static final String KEY_DATA_PREVIOUS = "dataPrevious";
private static final String KEY_DATA_NONE = "dataNone";
private static final String KEY_PATH = "path";
private static final String KEY_EXISTS = "exists";
private static final String KEY_META = "metadata";
Expand Down Expand Up @@ -118,15 +121,39 @@ static WritableMap snapshotToWritableMap(
getServerTimestampBehavior(appName, databaseId);

if (documentSnapshot.exists()) {
if (documentSnapshot.getData(timestampBehavior) != null) {
documentMap.putMap(
KEY_DATA, objectMapToWritable(documentSnapshot.getData(timestampBehavior)));
Map<String, Object> data = documentSnapshot.getData(timestampBehavior);
putSnapshotData(documentMap, KEY_DATA, data);

if (timestampBehavior != DocumentSnapshot.ServerTimestampBehavior.ESTIMATE) {
putSnapshotData(
documentMap,
KEY_DATA_ESTIMATE,
documentSnapshot.getData(DocumentSnapshot.ServerTimestampBehavior.ESTIMATE));
}
if (timestampBehavior != DocumentSnapshot.ServerTimestampBehavior.PREVIOUS) {
putSnapshotData(
documentMap,
KEY_DATA_PREVIOUS,
documentSnapshot.getData(DocumentSnapshot.ServerTimestampBehavior.PREVIOUS));
}
if (timestampBehavior != DocumentSnapshot.ServerTimestampBehavior.NONE) {
putSnapshotData(
documentMap,
KEY_DATA_NONE,
documentSnapshot.getData(DocumentSnapshot.ServerTimestampBehavior.NONE));
}
}

return documentMap;
}

private static void putSnapshotData(
WritableMap documentMap, String key, @Nullable Map<String, Object> data) {
if (data != null) {
documentMap.putMap(key, objectMapToWritable(data));
}
}

/**
* Convert a Firestore QuerySnapshot instance to a RN serializable WritableMap type map
*
Expand Down
5 changes: 5 additions & 0 deletions packages/firestore/consumer-type-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,12 @@ nsQuery.get().then((snap: FirebaseFirestoreTypes.QuerySnapshot) => {
nsDocRef.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) => {
if (snap.exists()) {
const d = snap.data();
const estimate = snap.data({ serverTimestamps: 'estimate' });
void d;
void estimate;
}
void snap.get('field');
void snap.get('field', { serverTimestamps: 'previous' });
void snap.metadata.isEqual(snap.metadata);
});

Expand Down Expand Up @@ -638,6 +641,8 @@ getAggregateFromServer(modQuery1, aggSpec).then(

// ----- getDoc, getDocFromCache, getDocFromServer -----
getDoc(modDoc).then(snap => snap.data());
getDoc(modDoc).then(snap => snap.data({ serverTimestamps: 'estimate' }));
getDoc(modDoc).then(snap => snap.get('field', { serverTimestamps: 'previous' }));
getDocFromCache(modDoc).then(snap => snap.data());
getDocFromServer(modDoc).then(snap => snap.data());

Expand Down
49 changes: 49 additions & 0 deletions packages/firestore/e2e/withConverter.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ const {
addDoc,
setDoc,
getDoc,
onSnapshot,
query,
where,
getDocs,
writeBatch,
increment,
serverTimestamp,
Timestamp,
initializeFirestore,
} = firestoreModular;

Expand Down Expand Up @@ -590,6 +593,52 @@ describe('firestore.withConverter', function () {
});
});

it("passes data() serverTimestamps options through converter snapshots", function () {
const timestampConverter = {
toFirestore() {
return {
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
};
},
fromFirestore(snapshot) {
return snapshot.data({ serverTimestamps: 'estimate' });
},
};

return withTestCollection(async coll => {
const ref = doc(coll, 'timestampConverter').withConverter(timestampConverter);
await new Promise((resolve, reject) => {
const unsubscribe = onSnapshot(
ref,
{ includeMetadataChanges: true },
snapshot => {
try {
if (!snapshot.exists() || !snapshot.metadata.hasPendingWrites) {
return;
}

const data = snapshot.data();
data.createdAt.should.be.an.instanceOf(Timestamp);
data.updatedAt.should.be.an.instanceOf(Timestamp);
unsubscribe();
resolve();
} catch (error) {
unsubscribe();
reject(error);
}
},
reject,
);

setDoc(ref, {}).catch(error => {
unsubscribe();
reject(error);
});
});
});
});

it('supports partials with merge', async function () {
return withTestCollection(async coll => {
const ref = doc(coll, 'post').withConverter(postConverterMerge);
Expand Down
31 changes: 30 additions & 1 deletion packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ @implementation RNFBFirestoreSerialize

static NSString *const KEY_PATH = @"path";
static NSString *const KEY_DATA = @"data";
static NSString *const KEY_DATA_ESTIMATE = @"dataEstimate";
static NSString *const KEY_DATA_PREVIOUS = @"dataPrevious";
static NSString *const KEY_DATA_NONE = @"dataNone";
static NSString *const KEY_EXISTS = @"exists";
static NSString *const KEY_CHANGES = @"changes";
static NSString *const KEY_METADATA = @"metadata";
Expand Down Expand Up @@ -188,6 +191,14 @@ + (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange
return changeMap;
}

+ (void)putSnapshotData:(NSMutableDictionary *)documentMap
key:(NSString *)key
data:(NSDictionary *)data {
if (data != nil) {
documentMap[key] = [self serializeDictionary:data];
}
}

// Native DocumentSnapshot -> NSDictionary (for JS)
+ (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot
firestoreKey:(NSString *)firestoreKey {
Expand Down Expand Up @@ -218,7 +229,25 @@ + (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot
}

NSDictionary *data = [snapshot dataWithServerTimestampBehavior:serverTimestampBehavior];
documentMap[KEY_DATA] = [self serializeDictionary:data];
[self putSnapshotData:documentMap key:KEY_DATA data:data];

if (serverTimestampBehavior != FIRServerTimestampBehaviorEstimate) {
NSDictionary *estimateData =
[snapshot dataWithServerTimestampBehavior:FIRServerTimestampBehaviorEstimate];
[self putSnapshotData:documentMap key:KEY_DATA_ESTIMATE data:estimateData];
}

if (serverTimestampBehavior != FIRServerTimestampBehaviorPrevious) {
NSDictionary *previousData =
[snapshot dataWithServerTimestampBehavior:FIRServerTimestampBehaviorPrevious];
[self putSnapshotData:documentMap key:KEY_DATA_PREVIOUS data:previousData];
}

if (serverTimestampBehavior != FIRServerTimestampBehaviorNone) {
NSDictionary *noneData =
[snapshot dataWithServerTimestampBehavior:FIRServerTimestampBehaviorNone];
[self putSnapshotData:documentMap key:KEY_DATA_NONE data:noneData];
}
}

return documentMap;
Expand Down
10 changes: 9 additions & 1 deletion packages/firestore/lib/FirestoreDocumentChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ const TYPE_MAP: Record<string, 'added' | 'modified' | 'removed'> = {

export interface DocumentChangeNativeData {
type: string;
doc: { path: string; data?: unknown; metadata?: [boolean, boolean]; exists?: boolean };
doc: {
path: string;
data?: unknown;
dataEstimate?: unknown;
dataPrevious?: unknown;
dataNone?: unknown;
metadata?: [boolean, boolean];
exists?: boolean;
};
ni: number;
oi: number;
isMetadataChange?: boolean;
Expand Down
41 changes: 38 additions & 3 deletions packages/firestore/lib/FirestoreDocumentSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ import type {
export interface DocumentSnapshotNativeData {
path: string;
data?: unknown;
dataEstimate?: unknown;
dataPrevious?: unknown;
dataNone?: unknown;
metadata?: [boolean, boolean];
exists?: boolean;
}
Expand All @@ -49,6 +52,9 @@ export default class DocumentSnapshot<
_firestore: FirestoreInternal;
_nativeData: DocumentSnapshotNativeData;
_data: Record<string, unknown> | undefined;
_dataEstimate: Record<string, unknown> | undefined;
_dataPrevious: Record<string, unknown> | undefined;
_dataNone: Record<string, unknown> | undefined;
_metadata: SnapshotMetadata;
_ref: DocumentReference<AppModelType, DbModelType>;
_exists: boolean;
Expand All @@ -63,6 +69,18 @@ export default class DocumentSnapshot<
this._nativeData = nativeData;
this._converter = converter;
this._data = parseNativeMap(firestore, nativeData.data as Record<string, unknown> | undefined);
this._dataEstimate = parseNativeMap(
firestore,
nativeData.dataEstimate as Record<string, unknown> | undefined,
);
this._dataPrevious = parseNativeMap(
firestore,
nativeData.dataPrevious as Record<string, unknown> | undefined,
);
this._dataNone = parseNativeMap(
firestore,
nativeData.dataNone as Record<string, unknown> | undefined,
);
this._metadata = new SnapshotMetadata(nativeData.metadata ?? [false, false]);
this._ref = new DocumentReference<AppModelType, DbModelType>(
firestore,
Expand All @@ -72,6 +90,20 @@ export default class DocumentSnapshot<
this._exists = nativeData.exists ?? false;
}

_dataForOptions(options?: SnapshotOptions): Record<string, unknown> | undefined {
// Older native payloads only include `data`; fall back to it if an option-specific map is absent.
switch (options?.serverTimestamps) {
case 'estimate':
return this._dataEstimate ?? this._data;
case 'previous':
return this._dataPrevious ?? this._data;
case 'none':
return this._dataNone ?? this._data;
default:
return this._data;
}
}

get id(): string {
return this._ref.id;
}
Expand Down Expand Up @@ -105,10 +137,10 @@ export default class DocumentSnapshot<
);
}
}
return this._data as AppModelType | undefined;
return this._dataForOptions(options) as AppModelType | undefined;
}

get(fieldPath: string | FieldPath, _options?: SnapshotOptions): DocumentFieldValueInternal {
get(fieldPath: string | FieldPath, options?: SnapshotOptions): DocumentFieldValueInternal {
if (!isString(fieldPath) && !(fieldPath instanceof FieldPath)) {
throw new Error(
"firebase.firestore() DocumentSnapshot.get(*) 'fieldPath' expected type string or FieldPath.",
Expand All @@ -129,7 +161,10 @@ export default class DocumentSnapshot<
path = fieldPath;
}

return extractFieldPathData(this._data, path._segments) as DocumentFieldValueInternal;
return extractFieldPathData(
this._dataForOptions(options),
path._segments,
) as DocumentFieldValueInternal;
}

isEqual(other: DocumentSnapshot<AppModelType, DbModelType>): boolean {
Expand Down
Loading
Loading