Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions docs/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>` |
| **return** | `Observable<FullMetadata>` |

#### TypeScript Example
```ts
Expand Down
11 changes: 7 additions & 4 deletions firestore/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

/**
Expand Down Expand Up @@ -298,10 +297,14 @@ export function collectionData<T=DocumentData, U extends string=never>(
);
}

export function collectionCountSnap(query: Query<unknown>): Observable<CountSnapshot> {
export function collectionCountSnap<AppModelType = DocumentData>(
query: Query<AppModelType>,
): Observable<CountSnapshot<AppModelType>> {
return from(getCountFromServer(query));
}

export function collectionCount(query: Query<unknown>): Observable<number> {
export function collectionCount<AppModelType = DocumentData>(
query: Query<AppModelType>,
): Observable<number> {
return collectionCountSnap(query).pipe(map((snap) => snap.data().count));
}
8 changes: 4 additions & 4 deletions firestore/document/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export function snapToData<T=DocumentData, R extends T=T>(
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;
}
8 changes: 8 additions & 0 deletions firestore/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ export type QuerySnapshot<T> = import('firebase/firestore').QuerySnapshot<T>;
export type DocumentChangeType = import('firebase/firestore').DocumentChangeType;
export type DocumentChange<T> = import('firebase/firestore').DocumentChange<T>;
export type QueryDocumentSnapshot<T> = import('firebase/firestore').QueryDocumentSnapshot<T>;
export type CountSnapshot<
AppModelType = DocumentData,
DbModelType extends DocumentData = DocumentData,
> = import('firebase/firestore').AggregateQuerySnapshot<
{count: import('firebase/firestore').AggregateField<number>},
AppModelType,
DbModelType
>;
16 changes: 12 additions & 4 deletions firestore/lite/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ export function collection<T=DocumentData>(query: Query<T>): Observable<QueryDoc
* Returns a stream of documents mapped to their data payload, and optionally the document ID
* @param query
*/
export function collectionData<T=DocumentData>(
export function collectionData<T=DocumentData, R extends T=T>(
query: Query<T>,
options: {
idField?: string
idField?: keyof R
}={},
): Observable<T[]> {
return collection(query).pipe(
Expand All @@ -48,10 +48,18 @@ export function collectionData<T=DocumentData>(
);
}

export function collectionCountSnap(query: Query<unknown>): Observable<CountSnapshot> {
export function collectionCountSnap<
AppModelType = DocumentData,
// DbModelType extends DocumentData = DocumentData,
>(query: Query<AppModelType>): Observable<CountSnapshot<AppModelType>> {
return from(getCount(query));
}

export function collectionCount(query: Query<unknown>): Observable<number> {
export function collectionCount<
AppModelType = DocumentData,
// DbModelType extends DocumentData = DocumentData
>(
query: Query<AppModelType>,
): Observable<number> {
return collectionCountSnap(query).pipe(map((snap) => snap.data().count));
}
24 changes: 12 additions & 12 deletions firestore/lite/document/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,30 @@ export function doc<T=DocumentData>(ref: DocumentReference<T>): Observable<Docum
* Returns a stream of a document, mapped to its data payload and optionally the document ID
* @param query
*/
export function docData<T=DocumentData>(
export function docData<T=DocumentData, R extends T=T>(
ref: DocumentReference<T>,
options: {
idField?: string
idField?: keyof R,
}={},
): Observable<T> {
): Observable<T | R | undefined> {
return doc(ref).pipe(map((snap) => snapToData(snap, options) as T));
}

export function snapToData<T=DocumentData>(
export function snapToData<T=DocumentData, R extends T=T>(
snapshot: DocumentSnapshot<T>,
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;
}
15 changes: 11 additions & 4 deletions firestore/lite/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import type * as lite from 'firebase/firestore/lite';

export type DocumentReference<T> = lite.DocumentReference<T>;
export type DocumentData = lite.DocumentData;
export type Query<T> = lite.Query<T>;
export type Query<AppModelType> = lite.Query<AppModelType>;
export type DocumentSnapshot<T> = lite.DocumentSnapshot<T>;
export type QuerySnapshot<T> = lite.QuerySnapshot<T>;
export type QueryDocumentSnapshot<T> = lite.QueryDocumentSnapshot<T>;
export type CountSnapshot = lite.AggregateQuerySnapshot<{
count: lite.AggregateField<number>;
}, any, DocumentData>;
export type CountSnapshot<
AppModelType = DocumentData,
DbModelType extends DocumentData = DocumentData,
> = lite.AggregateQuerySnapshot<
{
count: lite.AggregateField<number>;
},
AppModelType,
DbModelType
>;
84 changes: 40 additions & 44 deletions performance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@ const trace$ = (traceId: string) => {
export const trace = <T = any>(name: string) => (source$: Observable<T>) => new Observable<T>((subscriber) => {
const traceSubscription = trace$(name).subscribe();
return source$.pipe(
tap(
() => traceSubscription.unsubscribe(),
() => {
},
() => traceSubscription.unsubscribe(),
),
tap({
next: () => traceSubscription.unsubscribe(),
complete: () => traceSubscription.unsubscribe(),
}),
).subscribe(subscriber);
});

Expand All @@ -73,14 +71,16 @@ export const traceUntil = <T = any>(
options?: { orComplete?: boolean },
) => (source$: Observable<T>) => new Observable<T>((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);
});

/**
Expand All @@ -98,23 +98,27 @@ export const traceWhile = <T = any>(
options?: { orComplete?: boolean },
) => (source$: Observable<T>) => new Observable<T>((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);
});

/**
Expand All @@ -126,13 +130,9 @@ export const traceWhile = <T = any>(
export const traceUntilComplete = <T = any>(name: string) => (source$: Observable<T>) => new Observable<T>((subscriber) => {
const traceSubscription = trace$(name).subscribe();
return source$.pipe(
tap(
() => {
},
() => {
},
() => traceSubscription.unsubscribe(),
),
tap({
complete: () => traceSubscription.unsubscribe(),
}),
).subscribe(subscriber);
});

Expand All @@ -145,12 +145,8 @@ export const traceUntilComplete = <T = any>(name: string) => (source$: Observabl
export const traceUntilFirst = <T = any>(name: string) => (source$: Observable<T>) => new Observable<T>((subscriber) => {
const traceSubscription = trace$(name).subscribe();
return source$.pipe(
tap(
() => traceSubscription.unsubscribe(),
() => {
},
() => {
},
),
tap({
next: () => traceSubscription.unsubscribe(),
}),
).subscribe(subscriber);
});
23 changes: 18 additions & 5 deletions storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UploadTaskSnapshot> {
return new Observable<UploadTaskSnapshot>((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;
Expand Down Expand Up @@ -74,9 +83,13 @@ export function getDownloadURL(ref: StorageReference): Observable<string> {
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<any> {
/**
* 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<FullMetadata> {
return from(_getMetadata(ref));
}

Expand Down
22 changes: 22 additions & 0 deletions test/firestore-lite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Folk, Folk & {UID: string}>(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', () => {
Expand Down
22 changes: 22 additions & 0 deletions test/firestore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Folk, Folk & {UID: string}>(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
Expand Down