From 556de4742e075e747853104a7a84c2e0479de4d5 Mon Sep 17 00:00:00 2001 From: Kiyohiko Heima Date: Fri, 19 Aug 2022 14:16:03 +0900 Subject: [PATCH 1/4] feat(useFirestore): support dependent queries --- packages/firebase/useFirestore/index.test.ts | 94 ++++++++++++++++---- packages/firebase/useFirestore/index.ts | 58 ++++++------ 2 files changed, 105 insertions(+), 47 deletions(-) diff --git a/packages/firebase/useFirestore/index.test.ts b/packages/firebase/useFirestore/index.test.ts index 5534ea5c944..28a4b485420 100644 --- a/packages/firebase/useFirestore/index.test.ts +++ b/packages/firebase/useFirestore/index.test.ts @@ -1,27 +1,89 @@ -import { ref } from 'vue-demi' -import { onSnapshot } from 'firebase/firestore' -import type { Mock } from 'vitest' +import { computed, nextTick, ref } from 'vue-demi' import { useFirestore } from './index' -vi.mock('firebase/firestore', () => ({ - onSnapshot: vi.fn(), -})) +const getMockSnapFromRef = (docRef: any) => ({ + id: `${docRef.path}-id`, + data: () => (docRef), +}) + +const getData = (docRef: any) => ({ + ...docRef.data(), + id: docRef.id, +}) + +const unsubscribe = vi.fn() + +vi.mock('firebase/firestore', () => { + const onSnapshot = vi.fn((docRef: any, callbackFn: (payload: any) => {}) => { + if (docRef.path.includes('//')) + throw new Error('Invalid segment') + + callbackFn({ + ...getMockSnapFromRef(docRef), + docs: [getMockSnapFromRef(docRef)], + }) + return unsubscribe + }) + return { onSnapshot } +}) describe('useFirestore', () => { beforeEach(() => { - (onSnapshot as Mock).mockClear() + unsubscribe.mockClear() + }) + + it('should get `users` collection data', () => { + const collectionRef = { path: 'users' } as any + const data = useFirestore(collectionRef) + expect(data.value).toMatchObject([getData(getMockSnapFromRef(collectionRef))]) + }) + + it('should get `users/userId` document data', () => { + const docRef = { path: 'users/userId' } as any + const data = useFirestore(docRef) + expect(data.value).toMatchObject(getData(getMockSnapFromRef(docRef))) + }) + + it('should get `posts` computed query data', () => { + const queryRef = { path: 'posts', orderBy: 'desc' } as any + const data = useFirestore(computed(() => queryRef)) + expect(data.value).toMatchObject([getData(getMockSnapFromRef(queryRef))]) + }) + + it('should get `todos` function query data', () => { + const collectionRef = { path: 'todos' } as any + const data = useFirestore(() => collectionRef) + expect(data.value).toMatchObject([getData(getMockSnapFromRef(collectionRef))]) + }) + + it('should get initial value when pass falsy value', () => { + const collectionRef = { path: 'todos' } as any + const falsy = computed(() => false) + const data = useFirestore(() => falsy.value && collectionRef, [{ id: 'default' }]) + expect(data.value).toMatchObject([{ 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 = { path: 'posts', orderBy: 'desc' } as any + const reactiveQueryRef = ref(queryRef) + const data = useFirestore(reactiveQueryRef) + expect(data.value).toMatchObject([getData(getMockSnapFromRef(reactiveQueryRef.value))]) + reactiveQueryRef.value = { ...queryRef, orderBy: 'asc' } + await nextTick() + expect(unsubscribe).toHaveBeenCalled() + expect(data.value).toMatchObject([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 data = useFirestore(() => !!userId.value && { path: `users/${userId.value}/posts` } as any, [{ id: 'default' }]) + expect(data.value).toMatchObject([{ id: 'default' }]) + userId.value = 'userId' + await nextTick() + expect(data.value).toMatchObject([getData(getMockSnapFromRef({ path: `users/${userId.value}/posts` }))]) + userId.value = '' + await nextTick() + expect(unsubscribe).toHaveBeenCalled() + expect(data.value).toMatchObject([{ id: 'default' }]) }) }) diff --git a/packages/firebase/useFirestore/index.ts b/packages/firebase/useFirestore/index.ts index 49251adfe10..16c925dd0ba 100644 --- a/packages/firebase/useFirestore/index.ts +++ b/packages/firebase/useFirestore/index.ts @@ -34,24 +34,24 @@ function isDocumentReference(docRef: any): docRef is DocumentReference { } export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRefOrFunc: (() => DocumentReference) | MaybeRef>, initialValue: T, options?: UseFirestoreOptions ): Ref export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRefOrFunc: (() => Query) | MaybeRef>, initialValue: T[], options?: UseFirestoreOptions ): Ref // nullable initial values export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRefOrFunc: (() => DocumentReference) | MaybeRef>, initialValue?: T | undefined, options?: UseFirestoreOptions, ): Ref export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRefOrFunc: (() => Query) | MaybeRef>, initialValue?: T[], options?: UseFirestoreOptions ): Ref @@ -63,7 +63,7 @@ export function useFirestore( * @see https://vueuse.org/useFirestore */ export function useFirestore( - maybeDocRef: MaybeRef>, + maybeDocRefOrFunc: (() => FirebaseDocRef) | MaybeRef>, initialValue: any = undefined, options: UseFirestoreOptions = {}, ) { @@ -72,41 +72,37 @@ export function useFirestore( autoDispose = true, } = options - const refOfDocRef = isRef(maybeDocRef) ? maybeDocRef : computed(() => maybeDocRef) + const refOfDocRef = typeof maybeDocRefOrFunc === 'function' + ? computed(() => maybeDocRefOrFunc()) + : isRef(maybeDocRefOrFunc) + ? maybeDocRefOrFunc + : computed(() => maybeDocRefOrFunc) - if (isDocumentReference(refOfDocRef.value)) { - const data = ref(initialValue) as Ref - let close = () => { } + let close = () => { } + const data = ref(initialValue) as Ref - watch(refOfDocRef, (docRef) => { - close() + watch(refOfDocRef, (docRef) => { + close() + if (!refOfDocRef.value) { + data.value = initialValue + } + else if (isDocumentReference(refOfDocRef.value)) { close = onSnapshot(docRef as DocumentReference, (snapshot) => { data.value = getData(snapshot) || null }, errorHandler) - }, { immediate: true }) + } + else { + close = onSnapshot(docRef as Query, (snapshot) => { + data.value = snapshot.docs.map(getData).filter(isDef) + }, errorHandler) + } + }, { immediate: true }) + if (autoDispose && !isDocumentReference(refOfDocRef.value)) { tryOnScopeDispose(() => { close() }) - - return data } - else { - const data = ref(initialValue) as Ref - let close = () => { } - watch(refOfDocRef, (docRef) => { - close() - close = onSnapshot(docRef as Query, (snapshot) => { - data.value = snapshot.docs.map(getData).filter(isDef) - }, errorHandler) - }, { immediate: true }) - - if (autoDispose) { - tryOnScopeDispose(() => { - close() - }) - } - return data - } + return data } From d474985e199a776fbfb5c686c547d92b7c24668f Mon Sep 17 00:00:00 2001 From: Kiyohiko Heima Date: Fri, 19 Aug 2022 14:30:54 +0900 Subject: [PATCH 2/4] docs(useFirestore): add usage of dependent queries --- packages/firebase/useFirestore/index.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/firebase/useFirestore/index.md b/packages/firebase/useFirestore/index.md index 0c0bac8742e..ca8bc4abaaa 100644 --- a/packages/firebase/useFirestore/index.md +++ b/packages/firebase/useFirestore/index.md @@ -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' @@ -26,6 +26,11 @@ const user = useFirestore(doc(db, 'users', 'my-user-id')) const postLimit = ref(10) const postsQuery = computed(() => query(collection(db, 'posts'), orderBy('createdAt', 'desc'), limit(postLimit.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 userData = useFirestore(() => !!userId.value && doc(db, 'users', userId.value), []) ``` ## Share across instances From b370e516cbe8361d6899ed05509cdd664ed61fc8 Mon Sep 17 00:00:00 2001 From: Kiyohiko Heima Date: Fri, 19 Aug 2022 14:49:06 +0900 Subject: [PATCH 3/4] docs(useFirestore): fix initial value of example --- packages/firebase/useFirestore/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/firebase/useFirestore/index.md b/packages/firebase/useFirestore/index.md index ca8bc4abaaa..f4d4e3aa285 100644 --- a/packages/firebase/useFirestore/index.md +++ b/packages/firebase/useFirestore/index.md @@ -23,14 +23,14 @@ 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 userData = useFirestore(() => !!userId.value && doc(db, 'users', userId.value), []) +const userData = useFirestore(() => !!userId.value && doc(db, 'users', userId.value), null) ``` ## Share across instances From 9c48f6a119b2ad0f667afc907c157cad6df10ee9 Mon Sep 17 00:00:00 2001 From: Kiyohiko Heima Date: Sun, 25 Sep 2022 17:06:38 +0900 Subject: [PATCH 4/4] fix(useFirestore): remove unnecessary function interface --- packages/firebase/useFirestore/index.md | 3 +- packages/firebase/useFirestore/index.test.ts | 74 ++++++++++++-------- packages/firebase/useFirestore/index.ts | 18 +++-- 3 files changed, 53 insertions(+), 42 deletions(-) diff --git a/packages/firebase/useFirestore/index.md b/packages/firebase/useFirestore/index.md index f4d4e3aa285..8dd6edb4cbe 100644 --- a/packages/firebase/useFirestore/index.md +++ b/packages/firebase/useFirestore/index.md @@ -30,7 +30,8 @@ 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 userData = useFirestore(() => !!userId.value && doc(db, 'users', userId.value), null) +const userQuery = computed(() => !!userId.value && doc(db, 'users', userId.value)) +const userData = useFirestore(userQuery, null) ``` ## Share across instances diff --git a/packages/firebase/useFirestore/index.test.ts b/packages/firebase/useFirestore/index.test.ts index 28a4b485420..7a3e47f8884 100644 --- a/packages/firebase/useFirestore/index.test.ts +++ b/packages/firebase/useFirestore/index.test.ts @@ -1,30 +1,47 @@ +import { collection, doc } from 'firebase/firestore' +import type { Firestore } from 'firebase/firestore' import { computed, nextTick, ref } from 'vue-demi' import { useFirestore } from './index' +const dummyFirestore = {} as Firestore + const getMockSnapFromRef = (docRef: any) => ({ id: `${docRef.path}-id`, data: () => (docRef), }) -const getData = (docRef: any) => ({ - ...docRef.data(), - id: docRef.id, -}) +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 onSnapshot = vi.fn((docRef: any, callbackFn: (payload: any) => {}) => { - if (docRef.path.includes('//')) + 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 } + return { onSnapshot, collection, doc } }) describe('useFirestore', () => { @@ -33,57 +50,52 @@ describe('useFirestore', () => { }) it('should get `users` collection data', () => { - const collectionRef = { path: 'users' } as any + const collectionRef = collection(dummyFirestore, 'users') const data = useFirestore(collectionRef) - expect(data.value).toMatchObject([getData(getMockSnapFromRef(collectionRef))]) + expect(data.value).toEqual([getData(getMockSnapFromRef(collectionRef))]) }) it('should get `users/userId` document data', () => { - const docRef = { path: 'users/userId' } as any + const docRef = doc(dummyFirestore, 'users/userId') const data = useFirestore(docRef) - expect(data.value).toMatchObject(getData(getMockSnapFromRef(docRef))) + expect(data.value).toEqual(getData(getMockSnapFromRef(docRef))) }) it('should get `posts` computed query data', () => { - const queryRef = { path: 'posts', orderBy: 'desc' } as any + const queryRef = collection(dummyFirestore, 'posts') const data = useFirestore(computed(() => queryRef)) - expect(data.value).toMatchObject([getData(getMockSnapFromRef(queryRef))]) - }) - - it('should get `todos` function query data', () => { - const collectionRef = { path: 'todos' } as any - const data = useFirestore(() => collectionRef) - expect(data.value).toMatchObject([getData(getMockSnapFromRef(collectionRef))]) + expect(data.value).toEqual([getData(getMockSnapFromRef(queryRef))]) }) it('should get initial value when pass falsy value', () => { - const collectionRef = { path: 'todos' } as any - const falsy = computed(() => false) - const data = useFirestore(() => falsy.value && collectionRef, [{ id: 'default' }]) - expect(data.value).toMatchObject([{ id: 'default' }]) + 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 get reactive query data & unsubscribe previous query when re-querying', async () => { - const queryRef = { path: 'posts', orderBy: 'desc' } as any + const queryRef = collection(dummyFirestore, 'posts') const reactiveQueryRef = ref(queryRef) const data = useFirestore(reactiveQueryRef) - expect(data.value).toMatchObject([getData(getMockSnapFromRef(reactiveQueryRef.value))]) - reactiveQueryRef.value = { ...queryRef, orderBy: 'asc' } + expect(data.value).toEqual([getData(getMockSnapFromRef(reactiveQueryRef.value))]) + reactiveQueryRef.value = collection(dummyFirestore, 'todos') await nextTick() expect(unsubscribe).toHaveBeenCalled() - expect(data.value).toMatchObject([getData(getMockSnapFromRef(reactiveQueryRef.value))]) + expect(data.value).toEqual([getData(getMockSnapFromRef(reactiveQueryRef.value))]) }) it('should get user data only when user id exists', async () => { const userId = ref('') - const data = useFirestore(() => !!userId.value && { path: `users/${userId.value}/posts` } as any, [{ id: 'default' }]) - expect(data.value).toMatchObject([{ id: 'default' }]) + 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).toMatchObject([getData(getMockSnapFromRef({ path: `users/${userId.value}/posts` }))]) + expect(data.value).toEqual([getData(getMockSnapFromRef(collection(dummyFirestore, `users/${userId.value}/posts`)))]) userId.value = '' await nextTick() expect(unsubscribe).toHaveBeenCalled() - expect(data.value).toMatchObject([{ id: 'default' }]) + expect(data.value).toEqual([{ id: 'default' }]) }) }) diff --git a/packages/firebase/useFirestore/index.ts b/packages/firebase/useFirestore/index.ts index 16c925dd0ba..a37352f4d77 100644 --- a/packages/firebase/useFirestore/index.ts +++ b/packages/firebase/useFirestore/index.ts @@ -34,24 +34,24 @@ function isDocumentReference(docRef: any): docRef is DocumentReference { } export function useFirestore( - maybeDocRefOrFunc: (() => DocumentReference) | MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue: T, options?: UseFirestoreOptions ): Ref export function useFirestore( - maybeDocRefOrFunc: (() => Query) | MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue: T[], options?: UseFirestoreOptions ): Ref // nullable initial values export function useFirestore( - maybeDocRefOrFunc: (() => DocumentReference) | MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue?: T | undefined, options?: UseFirestoreOptions, ): Ref export function useFirestore( - maybeDocRefOrFunc: (() => Query) | MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue?: T[], options?: UseFirestoreOptions ): Ref @@ -63,7 +63,7 @@ export function useFirestore( * @see https://vueuse.org/useFirestore */ export function useFirestore( - maybeDocRefOrFunc: (() => FirebaseDocRef) | MaybeRef>, + maybeDocRef: MaybeRef | false>, initialValue: any = undefined, options: UseFirestoreOptions = {}, ) { @@ -72,11 +72,9 @@ export function useFirestore( autoDispose = true, } = options - const refOfDocRef = typeof maybeDocRefOrFunc === 'function' - ? computed(() => maybeDocRefOrFunc()) - : isRef(maybeDocRefOrFunc) - ? maybeDocRefOrFunc - : computed(() => maybeDocRefOrFunc) + const refOfDocRef = isRef(maybeDocRef) + ? maybeDocRef + : computed(() => maybeDocRef) let close = () => { } const data = ref(initialValue) as Ref