diff --git a/docs/storage.md b/docs/storage.md index 55f8d01..3eeff55 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -90,14 +90,14 @@ getDownloadURL(davidRef) ``` ### `getMetadata()` -The `getMetadata()` function creates an observable that emits the URL of the file's metadta. +The `getMetadata()` function creates an observable that emits the full set of object metadata, including read-only properties.. | | | |-----------------|------------------------------------------| | **function** | `getMetadata()` | | **params** | `import('firebase/storage').StorageReference` | | **import path** | `rxfire/storage` | -| **return** | `Observable` | +| **return** | `Observable` | #### TypeScript Example ```ts diff --git a/firestore/collection/index.ts b/firestore/collection/index.ts index 1a2ae88..ef0a1a2 100644 --- a/firestore/collection/index.ts +++ b/firestore/collection/index.ts @@ -33,9 +33,8 @@ import { pairwise, } from 'rxjs/operators'; import {snapToData} from '../document'; -import {DocumentChangeType, DocumentChange, Query, QueryDocumentSnapshot, QuerySnapshot, DocumentData} from '../interfaces'; +import {DocumentChangeType, DocumentChange, Query, QueryDocumentSnapshot, QuerySnapshot, DocumentData, CountSnapshot} from '../interfaces'; import {SnapshotOptions, getCountFromServer, refEqual} from 'firebase/firestore'; -import {CountSnapshot} from '../lite/interfaces'; const ALL_EVENTS: DocumentChangeType[] = ['added', 'modified', 'removed']; /** @@ -298,10 +297,14 @@ export function collectionData( ); } -export function collectionCountSnap(query: Query): Observable { +export function collectionCountSnap( + query: Query, +): Observable> { return from(getCountFromServer(query)); } -export function collectionCount(query: Query): Observable { +export function collectionCount( + query: Query, +): Observable { return collectionCountSnap(query).pipe(map((snap) => snap.data().count)); } diff --git a/firestore/document/index.ts b/firestore/document/index.ts index 0025501..f42efb0 100644 --- a/firestore/document/index.ts +++ b/firestore/document/index.ts @@ -54,8 +54,8 @@ export function snapToData( return data; } - return { - ...data, - [options.idField]: snapshot.id, - }; + // Preserve converter instances and custom prototypes by mutating the original object + // instead of creating a new one with spread syntax. + Object.assign(data, {[options.idField]: snapshot.id}); + return data; } diff --git a/firestore/interfaces.ts b/firestore/interfaces.ts index 0341fad..9dfea0a 100644 --- a/firestore/interfaces.ts +++ b/firestore/interfaces.ts @@ -7,3 +7,11 @@ export type QuerySnapshot = import('firebase/firestore').QuerySnapshot; export type DocumentChangeType = import('firebase/firestore').DocumentChangeType; export type DocumentChange = import('firebase/firestore').DocumentChange; export type QueryDocumentSnapshot = import('firebase/firestore').QueryDocumentSnapshot; +export type CountSnapshot< + AppModelType = DocumentData, + DbModelType extends DocumentData = DocumentData, +> = import('firebase/firestore').AggregateQuerySnapshot< + {count: import('firebase/firestore').AggregateField}, + AppModelType, + DbModelType +>; diff --git a/firestore/lite/collection/index.ts b/firestore/lite/collection/index.ts index 2bab09c..412d1d6 100644 --- a/firestore/lite/collection/index.ts +++ b/firestore/lite/collection/index.ts @@ -35,10 +35,10 @@ export function collection(query: Query): Observable( +export function collectionData( query: Query, options: { - idField?: string + idField?: keyof R }={}, ): Observable { return collection(query).pipe( @@ -48,10 +48,18 @@ export function collectionData( ); } -export function collectionCountSnap(query: Query): Observable { +export function collectionCountSnap< + AppModelType = DocumentData, + // DbModelType extends DocumentData = DocumentData, +>(query: Query): Observable> { return from(getCount(query)); } -export function collectionCount(query: Query): Observable { +export function collectionCount< + AppModelType = DocumentData, + // DbModelType extends DocumentData = DocumentData +>( + query: Query, +): Observable { return collectionCountSnap(query).pipe(map((snap) => snap.data().count)); } diff --git a/firestore/lite/document/index.ts b/firestore/lite/document/index.ts index 6fd8849..f126ad3 100644 --- a/firestore/lite/document/index.ts +++ b/firestore/lite/document/index.ts @@ -29,30 +29,30 @@ export function doc(ref: DocumentReference): Observable( +export function docData( ref: DocumentReference, options: { - idField?: string + idField?: keyof R, }={}, -): Observable { +): Observable { return doc(ref).pipe(map((snap) => snapToData(snap, options) as T)); } -export function snapToData( +export function snapToData( snapshot: DocumentSnapshot, options: { - idField?: string, + idField?: keyof R, }={}, -): {} | undefined { - // TODO clean up the typings - const data = snapshot.data() as any; +): T | R | undefined { + const data = snapshot.data(); // match the behavior of the JS SDK when the snapshot doesn't exist // it's possible with data converters too that the user didn't return an object - if (!snapshot.exists() || typeof data !== 'object' || data === null) { + if (!snapshot.exists() || typeof data !== 'object' || data === null || !options.idField) { return data; } - if (options.idField) { - data[options.idField] = snapshot.id; - } + + // Preserve converter instances and custom prototypes by mutating the original object + // instead of creating a new one with spread syntax. + Object.assign(data, {[options.idField]: snapshot.id}); return data; } diff --git a/firestore/lite/interfaces.ts b/firestore/lite/interfaces.ts index a9ba75c..d4d64e2 100644 --- a/firestore/lite/interfaces.ts +++ b/firestore/lite/interfaces.ts @@ -2,10 +2,17 @@ import type * as lite from 'firebase/firestore/lite'; export type DocumentReference = lite.DocumentReference; export type DocumentData = lite.DocumentData; -export type Query = lite.Query; +export type Query = lite.Query; export type DocumentSnapshot = lite.DocumentSnapshot; export type QuerySnapshot = lite.QuerySnapshot; export type QueryDocumentSnapshot = lite.QueryDocumentSnapshot; -export type CountSnapshot = lite.AggregateQuerySnapshot<{ - count: lite.AggregateField; -}, any, DocumentData>; +export type CountSnapshot< + AppModelType = DocumentData, + DbModelType extends DocumentData = DocumentData, +> = lite.AggregateQuerySnapshot< + { + count: lite.AggregateField; + }, + AppModelType, + DbModelType +>; diff --git a/performance/index.ts b/performance/index.ts index 0e05cd2..fa5b34b 100644 --- a/performance/index.ts +++ b/performance/index.ts @@ -50,12 +50,10 @@ const trace$ = (traceId: string) => { export const trace = (name: string) => (source$: Observable) => new Observable((subscriber) => { const traceSubscription = trace$(name).subscribe(); return source$.pipe( - tap( - () => traceSubscription.unsubscribe(), - () => { - }, - () => traceSubscription.unsubscribe(), - ), + tap({ + next: () => traceSubscription.unsubscribe(), + complete: () => traceSubscription.unsubscribe(), + }), ).subscribe(subscriber); }); @@ -73,14 +71,16 @@ export const traceUntil = ( options?: { orComplete?: boolean }, ) => (source$: Observable) => new Observable((subscriber) => { const traceSubscription = trace$(name).subscribe(); - return source$.pipe( - tap( - (a) => test(a) && traceSubscription.unsubscribe(), - () => { - }, - () => options && options.orComplete && traceSubscription.unsubscribe(), - ), - ).subscribe(subscriber); + return source$ + .pipe( + tap({ + next: (value) => test(value) && traceSubscription.unsubscribe(), + complete: () => + options && + options.orComplete && + traceSubscription.unsubscribe(), + }), + ).subscribe(subscriber); }); /** @@ -98,23 +98,27 @@ export const traceWhile = ( options?: { orComplete?: boolean }, ) => (source$: Observable) => new Observable((subscriber) => { let traceSubscription: Subscription | undefined; - return source$.pipe( - tap( - (a) => { - if (test(a)) { - traceSubscription = traceSubscription || trace$(name).subscribe(); - } else { - if (traceSubscription) { - traceSubscription.unsubscribe(); + return source$ + .pipe( + tap({ + next: (value) => { + if (test(value)) { + traceSubscription = + traceSubscription || trace$(name).subscribe(); + } else { + if (traceSubscription) { + traceSubscription.unsubscribe(); + } + traceSubscription = undefined; } - traceSubscription = undefined; - } - }, - () => { - }, - () => options && options.orComplete && traceSubscription && traceSubscription.unsubscribe(), - ), - ).subscribe(subscriber); + }, + complete: () => + options && + options.orComplete && + traceSubscription && + traceSubscription.unsubscribe(), + }), + ).subscribe(subscriber); }); /** @@ -126,13 +130,9 @@ export const traceWhile = ( export const traceUntilComplete = (name: string) => (source$: Observable) => new Observable((subscriber) => { const traceSubscription = trace$(name).subscribe(); return source$.pipe( - tap( - () => { - }, - () => { - }, - () => traceSubscription.unsubscribe(), - ), + tap({ + complete: () => traceSubscription.unsubscribe(), + }), ).subscribe(subscriber); }); @@ -145,12 +145,8 @@ export const traceUntilComplete = (name: string) => (source$: Observabl export const traceUntilFirst = (name: string) => (source$: Observable) => new Observable((subscriber) => { const traceSubscription = trace$(name).subscribe(); return source$.pipe( - tap( - () => traceSubscription.unsubscribe(), - () => { - }, - () => { - }, - ), + tap({ + next: () => traceSubscription.unsubscribe(), + }), ).subscribe(subscriber); }); diff --git a/storage/index.ts b/storage/index.ts index 9dad2d8..6b6c085 100644 --- a/storage/index.ts +++ b/storage/index.ts @@ -7,14 +7,23 @@ import { import {Observable, from} from 'rxjs'; import {map, shareReplay} from 'rxjs/operators'; -import type {UploadTaskSnapshot, StorageReference, UploadMetadata, StringFormat, UploadTask, UploadResult} from 'firebase/storage'; +import type { + UploadTaskSnapshot, + StorageReference, + UploadMetadata, + StringFormat, + UploadTask, + UploadResult, + FullMetadata, + StorageError, +} from 'firebase/storage'; export function fromTask(task: UploadTask): Observable { return new Observable((subscriber) => { let lastSnapshot: UploadTaskSnapshot | null = null; let complete = false; let hasError = false; - let error: any = null; + let error: StorageError | null = null; const emit = (snapshot: UploadTaskSnapshot) => { lastSnapshot = snapshot; @@ -74,9 +83,13 @@ export function getDownloadURL(ref: StorageReference): Observable { return from(_getDownloadURL(ref)); } -// TODO: fix storage typing in firebase, then apply the same fix here -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getMetadata(ref: StorageReference): Observable { +/** + * Retrieves the metadata for a given storage reference. + * + * @param ref The storage reference for which to retrieve metadata. + * @returns An observable that emits the metadata for the given reference. + */ +export function getMetadata(ref: StorageReference): Observable { return from(_getMetadata(ref)); } diff --git a/test/firestore-lite.test.ts b/test/firestore-lite.test.ts index 18ff4cb..bd0d561 100644 --- a/test/firestore-lite.test.ts +++ b/test/firestore-lite.test.ts @@ -214,6 +214,28 @@ describe('RxFire firestore/lite', () => { }); }); }); + + it('docData should preserve converter instances when idField is set', (done: jest.DoneCallback) => { + class Folk { + constructor(public name: string) {} + static fromFirestore(snap: QueryDocumentSnapshot) { + return new Folk(snap.data().name); + } + static toFirestore(model: Folk) { + return model; + } + } + + seedTest(firestore).then(({davidDoc}) => { + const unwrapped = docData(davidDoc.withConverter(Folk), {idField: 'UID'}); + + unwrapped.pipe(take(1)).subscribe((val) => { + expect(val).toBeInstanceOf(Folk); + expect(val).toEqual(expect.objectContaining({name: 'David', UID: 'david'})); + done(); + }); + }); + }); }); describe('Aggregations', () => { diff --git a/test/firestore.test.ts b/test/firestore.test.ts index 0274c3a..b65f18a 100644 --- a/test/firestore.test.ts +++ b/test/firestore.test.ts @@ -416,6 +416,28 @@ describe('RxFire Firestore', () => { }); }); + it('docData should preserve converter instances when idField is set', (done: jest.DoneCallback) => { + class Folk { + constructor(public name: string) {} + static fromFirestore(snap: QueryDocumentSnapshot) { + return new Folk(snap.data().name); + } + static toFirestore(model: Folk) { + return model; + } + } + + seedTest(firestore).then(({davidDoc}) => { + const unwrapped = docData(davidDoc.withConverter(Folk), {idField: 'UID'}); + + unwrapped.pipe(take(1)).subscribe((val) => { + expect(val).toBeInstanceOf(Folk); + expect(val).toEqual(expect.objectContaining({name: 'David', UID: 'david'})); + done(); + }); + }); + }); + /** * TODO(jamesdaniels) * Having trouble gettings these test green with the emulators