Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: useAggregateFromServer #192

Merged
merged 18 commits into from
Oct 12, 2023
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ yarn add react-firehooks

## Compatibility

- [firebase](https://www.npmjs.com/package/firebase): 9.11.0 or later
- [firebase](https://www.npmjs.com/package/firebase): 10.5.0 or later
- [react](https://www.npmjs.com/package/react): 16.8.0 or later

## Usage
Expand Down
19 changes: 19 additions & 0 deletions docs/firestore.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@
import { ... } from 'react-firehooks/firestore';
```

#### useAggregateFromServer

Returns aggregate of a Firestore Query. Does not update the result once initially calculated.

```javascript
const [data, loading, error] = useAggregateFromServer(query, aggregateSpec);
```

Params:

- `query`: Firestore query the aggregate is calculated for
- `aggregateSpec`: Aggregate specification

Returns:

- `value`: Aggregate of the Firestore query; `undefined` if the aggregate is currently being calculated, or an error occurred
- `loading`: `true` while calculating the aggregate; `false` if the aggregate was calculated successfully or an error occurred
- `error`: `undefined` if no error occurred

## useCountFromServer

Returns the number of documents in the result set of of a Firestore Query. Does not update the count once initially calculated.
Expand Down
5 changes: 5 additions & 0 deletions migrations/v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Migrate from v3 to v4

## Peer dependency

This library now requires firebase 10.5.0 or later
1,556 changes: 628 additions & 928 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"firebase": "9.11.0",
"firebase": "10.5.0",
"happy-dom": "12.2.2",
"husky": "8.0.3",
"lint-staged": "14.0.1",
Expand All @@ -91,7 +91,7 @@
"lint-staged": "lint-staged"
},
"peerDependencies": {
"firebase": "^9.11.0 || ^10.0.0",
"firebase": "^10.5.0",
"react": ">=16.8.0"
},
"lint-staged": {
Expand Down
1 change: 1 addition & 0 deletions src/firestore/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./types.js";
export * from "./useAggregateFromServer.js";
export * from "./useCountFromServer.js";
export * from "./useDocument.js";
export * from "./useDocumentData.js";
Expand Down
36 changes: 26 additions & 10 deletions src/firestore/internal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
aggregateFieldEqual,
AggregateSpec,
DocumentData,
DocumentReference,
DocumentSnapshot,
Expand All @@ -18,10 +20,10 @@ import type { Source } from "./types.js";
/**
* @internal
*/
export async function getDocFromSource<Value extends DocumentData = DocumentData>(
reference: DocumentReference<Value>,
export async function getDocFromSource<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
reference: DocumentReference<AppModelType, DbModelType>,
source: Source,
): Promise<DocumentSnapshot<Value>> {
): Promise<DocumentSnapshot<AppModelType, DbModelType>> {
switch (source) {
case "cache":
return await getDocFromCache(reference);
Expand All @@ -35,10 +37,10 @@ export async function getDocFromSource<Value extends DocumentData = DocumentData
/**
* @internal
*/
export async function getDocsFromSource<Value extends DocumentData = DocumentData>(
query: Query<Value>,
export async function getDocsFromSource<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
query: Query<AppModelType, DbModelType>,
source: Source,
): Promise<QuerySnapshot<Value>> {
): Promise<QuerySnapshot<AppModelType, DbModelType>> {
switch (source) {
case "cache":
return await getDocsFromCache(query);
Expand All @@ -52,9 +54,9 @@ export async function getDocsFromSource<Value extends DocumentData = DocumentDat
/**
* @internal
*/
export function isDocRefEqual<Value>(
a: DocumentReference<Value> | undefined,
b: DocumentReference<Value> | undefined,
export function isDocRefEqual<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
a: DocumentReference<AppModelType, DbModelType> | undefined,
b: DocumentReference<AppModelType, DbModelType> | undefined,
): boolean {
const areBothUndefined = a === undefined && b === undefined;
const areSameRef = a !== undefined && b !== undefined && refEqual(a, b);
Expand All @@ -64,8 +66,22 @@ export function isDocRefEqual<Value>(
/**
* @internal
*/
export function isQueryEqual<Value>(a: Query<Value> | undefined, b: Query<Value> | undefined): boolean {
export function isQueryEqual<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
a: Query<AppModelType, DbModelType> | undefined,
b: Query<AppModelType, DbModelType> | undefined,
): boolean {
const areBothUndefined = a === undefined && b === undefined;
const areSameRef = a !== undefined && b !== undefined && queryEqual(a, b);
return areBothUndefined || areSameRef;
}

/**
* @internal
*/
export function isAggregateSpecEqual<T extends AggregateSpec>(a: T, b: T): boolean {
if (Object.keys(a).length === Object.keys(b).length) {
return false;
}

return Object.entries(a).every(([key, value]) => aggregateFieldEqual(value, b[key]));
}
57 changes: 57 additions & 0 deletions src/firestore/useAggregateFromServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { AggregateSpec, AggregateSpecData, FirestoreError, Query } from "firebase/firestore";
import { getAggregateFromServer } from "firebase/firestore";
import type { ValueHookResult } from "../common/types.js";
import { useGet } from "../internal/useGet.js";
import { isAggregateSpecEqual, isQueryEqual } from "./internal.js";

export type UseAggregateFromServerResult<T extends AggregateSpec> = ValueHookResult<AggregateSpecData<T>, FirestoreError>;

interface Reference<T extends AggregateSpec> {
query: Query<unknown>;
aggregateSpec: T;
}

// eslint-disable-next-line jsdoc/require-param, jsdoc/require-returns
/**
* @internal
*/
async function getData<T extends AggregateSpec>({ query, aggregateSpec }: Reference<T>): Promise<AggregateSpecData<T>> {
const snap = await getAggregateFromServer(query, aggregateSpec);
return snap.data();
}

// eslint-disable-next-line jsdoc/require-param, jsdoc/require-returns
/**
* @internal
*/
function isEqual<TAggregateSpec extends AggregateSpec, TReference extends Reference<TAggregateSpec>>(
a: TReference | undefined,
b: TReference | undefined,
): boolean {
if (a === undefined && b === undefined) {
return true;
}

const areSameRef =
a !== undefined &&
b !== undefined &&
isQueryEqual(a.query, b.query) &&
isAggregateSpecEqual(a.aggregateSpec, a.aggregateSpec);
return areSameRef;
}

/**
* Returns aggregate of a Firestore Query. Does not update the result once initially calculated.
* @param query Firestore query the aggregate is calculated for
* @param aggregateSpec Aggregate specification
* @returns Size of the result set, loading state, and error
* - value: Aggregate of the Firestore query; `undefined` if the aggregate is currently being calculated, or an error occurred
* - loading: `true` while calculating the aggregate; `false` if the aggregate was calculated successfully or an error occurred
* - error: `undefined` if no error occurred
*/
export function useAggregateFromServer<T extends AggregateSpec>(
query: Query<unknown> | undefined | null,
aggregateSpec: T,
): UseAggregateFromServerResult<T> {
return useGet(query ? { query, aggregateSpec } : undefined, getData, isEqual);
}
22 changes: 12 additions & 10 deletions src/firestore/useDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import { useListen, UseListenOnChange } from "../internal/useListen.js";
import { LoadingState } from "../internal/useLoadingValue.js";
import { isDocRefEqual } from "./internal.js";

export type UseDocumentResult<Value extends DocumentData = DocumentData> = ValueHookResult<
DocumentSnapshot<Value>,
FirestoreError
>;
export type UseDocumentResult<AppModelType = DocumentData> = ValueHookResult<DocumentSnapshot<AppModelType>, FirestoreError>;

/**
* Options to configure the subscription
Expand All @@ -26,24 +23,29 @@ export interface UseDocumentOptions {

/**
* Returns and updates a DocumentSnapshot of a Firestore DocumentReference
* @template Value Type of the document data
* @template AppModelType Shape of the data after it was converted from firestore
* @template DbModelType Shape of the data in firestore
* @param reference Firestore DocumentReference that will be subscribed to
* @param options Options to configure the subscription
* @returns Document snapshot, loading state, and error
* - value: DocumentSnapshot; `undefined` if document does not exist, is currently being fetched, or an error occurred
* - loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred
* - error: `undefined` if no error occurred
*/
export function useDocument<Value extends DocumentData = DocumentData>(
reference: DocumentReference<Value> | undefined | null,
export function useDocument<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
reference: DocumentReference<AppModelType, DbModelType> | undefined | null,
options?: UseDocumentOptions,
): UseDocumentResult<Value> {
): UseDocumentResult<AppModelType> {
const { snapshotListenOptions } = options ?? {};
const { includeMetadataChanges } = snapshotListenOptions ?? {};

const onChange: UseListenOnChange<DocumentSnapshot<Value>, FirestoreError, DocumentReference<Value>> = useCallback(
const onChange: UseListenOnChange<
DocumentSnapshot<AppModelType, DbModelType>,
FirestoreError,
DocumentReference<AppModelType, DbModelType>
> = useCallback(
(stableRef, next, error) =>
onSnapshot<Value>(
onSnapshot(
stableRef,
{ includeMetadataChanges },
{
Expand Down
25 changes: 15 additions & 10 deletions src/firestore/useDocumentData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@ import { useListen, UseListenOnChange } from "../internal/useListen.js";
import { LoadingState } from "../internal/useLoadingValue.js";
import { isDocRefEqual } from "./internal.js";

export type UseDocumentDataResult<Value extends DocumentData = DocumentData> = ValueHookResult<Value, FirestoreError>;
export type UseDocumentDataResult<AppModelType = DocumentData> = ValueHookResult<AppModelType, FirestoreError>;

/**
* Options to configure the subscription
*/
export interface UseDocumentDataOptions<Value extends DocumentData = DocumentData> {
export interface UseDocumentDataOptions<AppModelType = DocumentData> {
snapshotListenOptions?: SnapshotListenOptions;
snapshotOptions?: SnapshotOptions;
initialValue?: Value;
initialValue?: AppModelType;
}

/**
* Returns and updates the data of a Firestore DocumentReference
* @template Value Type of the document data
* @template AppModelType Shape of the data after it was converted from firestore
* @template DbModelType Shape of the data in firestore
* @param reference Firestore DocumentReference that will be subscribed to
* @param options Options to configure the subscription
* `initialValue`: Value that is returned while the document is being fetched.
Expand All @@ -34,17 +35,21 @@ export interface UseDocumentDataOptions<Value extends DocumentData = DocumentDat
* - loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred
* - error: `undefined` if no error occurred
*/
export function useDocumentData<Value extends DocumentData = DocumentData>(
reference: DocumentReference<Value> | undefined | null,
options?: UseDocumentDataOptions<Value>,
): UseDocumentDataResult<Value> {
export function useDocumentData<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
reference: DocumentReference<AppModelType, DbModelType> | undefined | null,
options?: UseDocumentDataOptions<AppModelType>,
): UseDocumentDataResult<AppModelType> {
const { snapshotListenOptions, snapshotOptions } = options ?? {};
const { includeMetadataChanges } = snapshotListenOptions ?? {};
const { serverTimestamps } = snapshotOptions ?? {};

const onChange: UseListenOnChange<Value, FirestoreError, DocumentReference<Value>> = useCallback(
const onChange: UseListenOnChange<
AppModelType,
FirestoreError,
DocumentReference<AppModelType, DbModelType>
> = useCallback(
(stableRef, next, error) =>
onSnapshot<Value>(
onSnapshot(
stableRef,
{ includeMetadataChanges },
{
Expand Down
13 changes: 7 additions & 6 deletions src/firestore/useDocumentDataOnce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useGet } from "../internal/useGet.js";
import { getDocFromSource, isDocRefEqual } from "./internal.js";
import type { Source } from "./types.js";

export type UseDocumentDataOnceResult<Value extends DocumentData = DocumentData> = ValueHookResult<Value, FirestoreError>;
export type UseDocumentDataOnceResult<AppModelType = DocumentData> = ValueHookResult<AppModelType, FirestoreError>;

/**
* Options to configure how the document is fetched
Expand All @@ -17,23 +17,24 @@ export interface UseDocumentDataOnceOptions {

/**
* Returns the data of a Firestore DocumentReference
* @template Value Type of the document data
* @template AppModelType Shape of the data after it was converted from firestore
* @template DbModelType Shape of the data in firestore
* @param reference Firestore DocumentReference that will be subscribed to
* @param options Options to configure how the document is fetched
* @returns Document data, loading state, and error
* - value: Document data; `undefined` if document does not exist, is currently being fetched, or an error occurred
* - loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred
* - error: `undefined` if no error occurred
*/
export function useDocumentDataOnce<Value extends DocumentData = DocumentData>(
reference: DocumentReference<Value> | undefined | null,
export function useDocumentDataOnce<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
reference: DocumentReference<AppModelType, DbModelType> | undefined | null,
options?: UseDocumentDataOnceOptions,
): UseDocumentDataOnceResult<Value> {
): UseDocumentDataOnceResult<AppModelType> {
const { source = "default", snapshotOptions } = options ?? {};
const { serverTimestamps } = snapshotOptions ?? {};

const getData = useCallback(
async (stableRef: DocumentReference<Value>) => {
async (stableRef: DocumentReference<AppModelType, DbModelType>) => {
const snap = await getDocFromSource(stableRef, source);
return snap.data({ serverTimestamps });
},
Expand Down
18 changes: 11 additions & 7 deletions src/firestore/useDocumentOnce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useGet } from "../internal/useGet.js";
import { getDocFromSource, isDocRefEqual } from "./internal.js";
import type { Source } from "./types.js";

export type UseDocumentOnceResult<Value extends DocumentData = DocumentData> = ValueHookResult<
DocumentSnapshot<Value>,
export type UseDocumentOnceResult<AppModelType = DocumentData> = ValueHookResult<
DocumentSnapshot<AppModelType>,
FirestoreError
>;

Expand All @@ -19,21 +19,25 @@ export interface UseDocumentOnceOptions {

/**
* Returns the DocumentSnapshot of a Firestore DocumentReference. Does not update the DocumentSnapshot once initially fetched
* @template Value Type of the document data
* @template AppModelType Shape of the data after it was converted from firestore
* @template DbModelType Shape of the data in firestore
* @param reference Firestore DocumentReference that will be fetched
* @param options Options to configure how the document is fetched
* @returns DocumentSnapshot, loading state, and error
* - value: DocumentSnapshot; `undefined` if document does not exist, is currently being fetched, or an error occurred
* - loading: `true` while fetching the document; `false` if the document was fetched successfully or an error occurred
* - error: `undefined` if no error occurred
*/
export function useDocumentOnce<Value extends DocumentData = DocumentData>(
reference: DocumentReference<Value> | undefined | null,
export function useDocumentOnce<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
reference: DocumentReference<AppModelType, DbModelType> | undefined | null,
options?: UseDocumentOnceOptions,
): UseDocumentOnceResult<Value> {
): UseDocumentOnceResult<AppModelType> {
const { source = "default" } = options ?? {};

const getData = useCallback((stableRef: DocumentReference<Value>) => getDocFromSource(stableRef, source), [source]);
const getData = useCallback(
(stableRef: DocumentReference<AppModelType, DbModelType>) => getDocFromSource(stableRef, source),
[source],
);

return useGet(reference ?? undefined, getData, isDocRefEqual);
}
Loading