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(useFirestore): support dependent queries #2103

Merged
merged 4 commits into from Sep 26, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}