Skip to content

Commit

Permalink
feat(useFirestore): support dependent queries (#2103)
Browse files Browse the repository at this point in the history
  • Loading branch information
kiyopikko committed Sep 26, 2022
1 parent 324ff52 commit 2188b85
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 50 deletions.
12 changes: 9 additions & 3 deletions packages/firebase/useFirestore/index.md
Expand Up @@ -8,7 +8,7 @@ Reactive [Firestore](https://firebase.google.com/docs/firestore) binding. Making

## Usage

```js {9,12,17}
```js {9,12,17,22}
import { computed, ref } from 'vue'
import { initializeApp } from 'firebase/app'
import { collection, doc, getFirestore, limit, orderBy, query } from 'firebase/firestore'
Expand All @@ -23,9 +23,15 @@ const todos = useFirestore(collection(db, 'todos'))
const user = useFirestore(doc(db, 'users', 'my-user-id'))

// you can also use ref value for reactive query
const postLimit = ref(10)
const postsQuery = computed(() => query(collection(db, 'posts'), orderBy('createdAt', 'desc'), limit(postLimit.value)))
const postsLimit = ref(10)
const postsQuery = computed(() => query(collection(db, 'posts'), orderBy('createdAt', 'desc'), limit(postsLimit.value)))
const posts = useFirestore(postsQuery)

// you can use the boolean value to tell a query when it is ready to run
// when it gets falsy value, return the initial value
const userId = ref('')
const userQuery = computed(() => !!userId.value && doc(db, 'users', userId.value))
const userData = useFirestore(userQuery, null)
```

## Share across instances
Expand Down
106 changes: 90 additions & 16 deletions packages/firebase/useFirestore/index.test.ts
@@ -1,27 +1,101 @@
import { ref } from 'vue-demi'
import { onSnapshot } from 'firebase/firestore'
import type { Mock } from 'vitest'
import { collection, doc } from 'firebase/firestore'
import type { Firestore } from 'firebase/firestore'
import { computed, nextTick, ref } from 'vue-demi'
import { useFirestore } from './index'

vi.mock('firebase/firestore', () => ({
onSnapshot: vi.fn(),
}))
const dummyFirestore = {} as Firestore

const getMockSnapFromRef = (docRef: any) => ({
id: `${docRef.path}-id`,
data: () => (docRef),
})

const getData = (docRef: any) => {
const data = docRef.data()
Object.defineProperty(data, 'id', {
value: docRef.id.toString(),
writable: false,
})
return data
}

const unsubscribe = vi.fn()

vi.mock('firebase/firestore', () => {
const doc = vi.fn((_: Firestore, path: string) => {
if (path.includes('//'))
throw new Error('Invalid segment')
return { path }
})

const collection = vi.fn((_: Firestore, path: string) => {
if (path.includes('//'))
throw new Error('Invalid segment')
return { path }
})

const onSnapshot = vi.fn((docRef: any, callbackFn: (payload: any) => {}) => {
callbackFn({
...getMockSnapFromRef(docRef),
docs: [getMockSnapFromRef(docRef)],
})
return unsubscribe
})
return { onSnapshot, collection, doc }
})

describe('useFirestore', () => {
beforeEach(() => {
(onSnapshot as Mock).mockClear()
unsubscribe.mockClear()
})

it('should get `users` collection data', () => {
const collectionRef = collection(dummyFirestore, 'users')
const data = useFirestore(collectionRef)
expect(data.value).toEqual([getData(getMockSnapFromRef(collectionRef))])
})

it('should get `users/userId` document data', () => {
const docRef = doc(dummyFirestore, 'users/userId')
const data = useFirestore(docRef)
expect(data.value).toEqual(getData(getMockSnapFromRef(docRef)))
})

it('should get `posts` computed query data', () => {
const queryRef = collection(dummyFirestore, 'posts')
const data = useFirestore(computed(() => queryRef))
expect(data.value).toEqual([getData(getMockSnapFromRef(queryRef))])
})

it('should get initial value when pass falsy value', () => {
const collectionRef = collection(dummyFirestore, 'todos')
const falsy = computed(() => false as boolean && collectionRef)
const data = useFirestore(falsy, [{ id: 'default' }])
expect(data.value).toEqual([{ id: 'default' }])
})

it('should call onSnapshot with document reference', () => {
const docRef = { path: 'users' } as any
useFirestore(docRef)
expect((onSnapshot as Mock).mock.calls[0][0]).toStrictEqual(docRef)
it('should get reactive query data & unsubscribe previous query when re-querying', async () => {
const queryRef = collection(dummyFirestore, 'posts')
const reactiveQueryRef = ref(queryRef)
const data = useFirestore(reactiveQueryRef)
expect(data.value).toEqual([getData(getMockSnapFromRef(reactiveQueryRef.value))])
reactiveQueryRef.value = collection(dummyFirestore, 'todos')
await nextTick()
expect(unsubscribe).toHaveBeenCalled()
expect(data.value).toEqual([getData(getMockSnapFromRef(reactiveQueryRef.value))])
})

it('should call onSnapshot with ref value of document reference', () => {
const docRef = { path: 'posts' } as any
const refOfDocRef = ref(docRef)
useFirestore(refOfDocRef)
expect((onSnapshot as Mock).mock.calls[0][0]).toStrictEqual(docRef)
it('should get user data only when user id exists', async () => {
const userId = ref('')
const queryRef = computed(() => !!userId.value && collection(dummyFirestore, `users/${userId.value}/posts`))
const data = useFirestore(queryRef, [{ id: 'default' }])
expect(data.value).toEqual([{ id: 'default' }])
userId.value = 'userId'
await nextTick()
expect(data.value).toEqual([getData(getMockSnapFromRef(collection(dummyFirestore, `users/${userId.value}/posts`)))])
userId.value = ''
await nextTick()
expect(unsubscribe).toHaveBeenCalled()
expect(data.value).toEqual([{ id: 'default' }])
})
})
56 changes: 25 additions & 31 deletions packages/firebase/useFirestore/index.ts
Expand Up @@ -34,24 +34,24 @@ function isDocumentReference<T>(docRef: any): docRef is DocumentReference<T> {
}

export function useFirestore<T extends DocumentData>(
maybeDocRef: MaybeRef<DocumentReference<T>>,
maybeDocRef: MaybeRef<DocumentReference<T> | false>,
initialValue: T,
options?: UseFirestoreOptions
): Ref<T | null>
export function useFirestore<T extends DocumentData>(
maybeDocRef: MaybeRef<Query<T>>,
maybeDocRef: MaybeRef<Query<T> | false>,
initialValue: T[],
options?: UseFirestoreOptions
): Ref<T[]>

// nullable initial values
export function useFirestore<T extends DocumentData>(
maybeDocRef: MaybeRef<DocumentReference<T>>,
maybeDocRef: MaybeRef<DocumentReference<T> | false>,
initialValue?: T | undefined,
options?: UseFirestoreOptions,
): Ref<T | undefined | null>
export function useFirestore<T extends DocumentData>(
maybeDocRef: MaybeRef<Query<T>>,
maybeDocRef: MaybeRef<Query<T> | false>,
initialValue?: T[],
options?: UseFirestoreOptions
): Ref<T[] | undefined>
Expand All @@ -63,7 +63,7 @@ export function useFirestore<T extends DocumentData>(
* @see https://vueuse.org/useFirestore
*/
export function useFirestore<T extends DocumentData>(
maybeDocRef: MaybeRef<FirebaseDocRef<T>>,
maybeDocRef: MaybeRef<FirebaseDocRef<T> | false>,
initialValue: any = undefined,
options: UseFirestoreOptions = {},
) {
Expand All @@ -72,41 +72,35 @@ export function useFirestore<T extends DocumentData>(
autoDispose = true,
} = options

const refOfDocRef = isRef(maybeDocRef) ? maybeDocRef : computed(() => maybeDocRef)
const refOfDocRef = isRef(maybeDocRef)
? maybeDocRef
: computed(() => maybeDocRef)

if (isDocumentReference<T>(refOfDocRef.value)) {
const data = ref(initialValue) as Ref<T | null | undefined>
let close = () => { }
let close = () => { }
const data = ref(initialValue) as Ref<T | T[] | null | undefined>

watch(refOfDocRef, (docRef) => {
close()
watch(refOfDocRef, (docRef) => {
close()
if (!refOfDocRef.value) {
data.value = initialValue
}
else if (isDocumentReference<T>(refOfDocRef.value)) {
close = onSnapshot(docRef as DocumentReference<T>, (snapshot) => {
data.value = getData(snapshot) || null
}, errorHandler)
}, { immediate: true })
}
else {
close = onSnapshot(docRef as Query<T>, (snapshot) => {
data.value = snapshot.docs.map(getData).filter(isDef)
}, errorHandler)
}
}, { immediate: true })

if (autoDispose && !isDocumentReference<T>(refOfDocRef.value)) {
tryOnScopeDispose(() => {
close()
})

return data
}
else {
const data = ref(initialValue) as Ref<T[] | undefined>
let close = () => { }

watch(refOfDocRef, (docRef) => {
close()
close = onSnapshot(docRef as Query<T>, (snapshot) => {
data.value = snapshot.docs.map(getData).filter(isDef)
}, errorHandler)
}, { immediate: true })

if (autoDispose) {
tryOnScopeDispose(() => {
close()
})
}
return data
}
return data
}

0 comments on commit 2188b85

Please sign in to comment.