Skip to content

Commit ab3c9cb

Browse files
committed
refactor: correct types and useCollection
1 parent dd8a54f commit ab3c9cb

File tree

7 files changed

+168
-100
lines changed

7 files changed

+168
-100
lines changed

Diff for: src/firestore/index.ts

+11-16
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ function unsubscribeAll(subs: Record<string, FirestoreSubscription>) {
4141
}
4242
}
4343

44-
function updateDataFromDocumentSnapshot(
44+
function updateDataFromDocumentSnapshot<T>(
4545
options: Required<FirestoreOptions>,
46-
target: CommonBindOptionsParameter['target'],
46+
target: Ref<T>,
4747
path: string,
48-
snapshot: DocumentSnapshot,
48+
snapshot: DocumentSnapshot<T>,
4949
subs: Record<string, FirestoreSubscription>,
5050
ops: CommonBindOptionsParameter['ops'],
5151
depth: number,
@@ -189,9 +189,9 @@ interface BindCollectionParameter extends CommonBindOptionsParameter {
189189

190190
// TODO: refactor without using an object to improve size like the other functions
191191

192-
export function bindCollection(
192+
export function bindCollection<T>(
193193
target: BindCollectionParameter['target'],
194-
collection: BindCollectionParameter['collection'],
194+
collection: CollectionReference<T> | Query<T>,
195195
ops: BindCollectionParameter['ops'],
196196
resolve: BindCollectionParameter['resolve'],
197197
reject: BindCollectionParameter['reject'],
@@ -209,7 +209,7 @@ export function bindCollection(
209209
const arraySubs: Record<string, FirestoreSubscription>[] = []
210210

211211
const change = {
212-
added: ({ newIndex, doc }: DocumentChange) => {
212+
added: ({ newIndex, doc }: DocumentChange<T>) => {
213213
arraySubs.splice(newIndex, 0, Object.create(null))
214214
const subs = arraySubs[newIndex]
215215
const [data, refs] = extractRefs(options.serialize(doc), undefined, subs)
@@ -225,7 +225,7 @@ export function bindCollection(
225225
resolve.bind(null, doc)
226226
)
227227
},
228-
modified: ({ oldIndex, newIndex, doc }: DocumentChange) => {
228+
modified: ({ oldIndex, newIndex, doc }: DocumentChange<T>) => {
229229
const array = unref(arrayRef)
230230
const subs = arraySubs[oldIndex]
231231
const oldData = array[oldIndex]
@@ -246,7 +246,7 @@ export function bindCollection(
246246
resolve
247247
)
248248
},
249-
removed: ({ oldIndex }: DocumentChange) => {
249+
removed: ({ oldIndex }: DocumentChange<T>) => {
250250
const array = unref(arrayRef)
251251
ops.remove(array, oldIndex)
252252
unsubscribeAll(arraySubs.splice(oldIndex, 1)[0])
@@ -262,12 +262,7 @@ export function bindCollection(
262262
// from the query appearing as added
263263
// (https://firebase.google.com/docs/firestore/query-data/listen#view_changes_between_snapshots)
264264

265-
const docChanges =
266-
/* istanbul ignore next */
267-
typeof snapshot.docChanges === 'function'
268-
? snapshot.docChanges()
269-
: /* istanbul ignore next to support firebase < 5*/
270-
(snapshot.docChanges as unknown as DocumentChange[])
265+
const docChanges = snapshot.docChanges()
271266

272267
if (!isResolved && docChanges.length) {
273268
// isResolved is only meant to make sure we do the check only once
@@ -333,9 +328,9 @@ interface BindDocumentParameter extends CommonBindOptionsParameter {
333328
* @param param0
334329
* @param extraOptions
335330
*/
336-
export function bindDocument(
331+
export function bindDocument<T>(
337332
target: BindDocumentParameter['target'],
338-
document: BindDocumentParameter['document'],
333+
document: DocumentReference<T>,
339334
ops: BindDocumentParameter['ops'],
340335
resolve: BindDocumentParameter['resolve'],
341336
reject: BindDocumentParameter['reject'],

Diff for: src/shared.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { DocumentData, DocumentReference } from 'firebase/firestore'
1+
import type {
2+
CollectionReference,
3+
DocumentData,
4+
DocumentReference,
5+
} from 'firebase/firestore'
26

37
// FIXME: replace any with unknown or T generics
48

@@ -79,6 +83,14 @@ export function isDocumentRef(o: any): o is DocumentReference {
7983
return isObject(o) && o.type === 'document'
8084
}
8185

86+
/**
87+
* Checks if a variable is a Firestore Collection Reference
88+
* @param o
89+
*/
90+
export function isCollectionRef(o: any): o is CollectionReference {
91+
return isObject(o) && o.type === 'collection'
92+
}
93+
8294
type ReferenceType = 'collection' | 'document' | 'query'
8395

8496
/**

Diff for: src/vuefire/firestore.ts

+34-27
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ import {
1616
App,
1717
ComponentPublicInstance,
1818
getCurrentInstance,
19+
getCurrentScope,
1920
isVue3,
2021
onBeforeUnmount,
22+
onScopeDispose,
2123
onUnmounted,
2224
ref,
2325
Ref,
2426
toRef,
27+
InjectionKey,
2528
} from 'vue-demi'
2629

2730
export const ops: OperationsType = {
@@ -234,7 +237,7 @@ export const firestorePlugin = function firestorePlugin(
234237
for (const key in refs) {
235238
this[bindName as '$bind'](
236239
key,
237-
// @ts-ignore: FIXME: there is probably a wrong type in global properties
240+
// @ts-expect-error: FIXME: there is probably a wrong type in global properties
238241
refs[key],
239242
globalOptions
240243
)
@@ -284,39 +287,43 @@ export function bind(
284287
return promise
285288
}
286289

287-
export function useFirestore<T>(
288-
docRef: DocumentReference<T>,
289-
options?: FirestoreOptions
290-
): [Ref<T | null>, Promise<T | null>, UnbindType]
291-
export function useFirestore<T>(
292-
collectionRef: Query<T> | CollectionReference<T>,
293-
options?: FirestoreOptions
294-
): [Ref<T[]>, Promise<T[]>, UnbindType]
295-
export function useFirestore<T>(
296-
docOrCollectionRef: CollectionReference<T> | Query<T> | DocumentReference<T>,
297-
options?: FirestoreOptions
290+
const pendingPromises = new Set<Promise<any>>()
291+
292+
// TODO: should be usable in different contexts, use inject, provide
293+
export function usePendingPromises() {
294+
return Promise.all(pendingPromises)
295+
}
296+
297+
export interface UseCollectionOptions {}
298+
299+
/**
300+
* Creates a reactive array of documents from a collection ref or a query from Firestore.
301+
*
302+
* @param collectionRef - query or collection
303+
* @param options - optional options
304+
* @returns
305+
*/
306+
export function useCollection<T>(
307+
collectionRef: CollectionReference<T> | Query<T>,
308+
options?: UseCollectionOptions
298309
) {
299-
const target =
300-
'where' in docOrCollectionRef ? ref<T | null>(null) : ref<T[]>([])
310+
const data = ref<T[]>()
301311

302-
let unbind: ReturnType<typeof bindCollection | typeof bindDocument>
312+
let unbind!: ReturnType<typeof bindCollection>
303313
const promise = new Promise((resolve, reject) => {
304-
unbind = ('where' in docOrCollectionRef ? bindCollection : bindDocument)(
305-
target,
306-
// the type is good because of the ternary
307-
docOrCollectionRef as any,
308-
ops,
309-
resolve,
310-
reject,
311-
options
312-
)
314+
unbind = bindCollection(data, collectionRef, ops, resolve, reject, options)
313315
})
314316

315-
if (getCurrentInstance()) {
316-
onUnmounted(() => unbind())
317+
// TODO: warning
318+
if (getCurrentScope()) {
319+
pendingPromises.add(promise)
320+
onScopeDispose(() => {
321+
pendingPromises.delete(promise)
322+
unbind()
323+
})
317324
}
318325

319-
return [target, promise, unbind!]
326+
return data
320327
}
321328

322329
export const unbind = (target: Ref, reset?: FirestoreOptions['reset']) =>

Diff for: src/vuefire/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ export {
33
firestorePlugin,
44
bind as firestoreBind,
55
unbind as firestoreUnbind,
6+
useCollection,
67
} from './firestore'
8+
9+
export type { UseCollectionOptions } from './firestore'

Diff for: tests/firestore.bind.spec.ts

-56
This file was deleted.

Diff for: tests/firestore/collection.spec.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { mount } from '@vue/test-utils'
2+
import { describe, expect, it } from 'vitest'
3+
import { useCollection } from '../../src'
4+
import { addDoc } from 'firebase/firestore'
5+
import { setupRefs } from '../utils'
6+
import { usePendingPromises } from '../../src/vuefire/firestore'
7+
8+
describe('Firestore collections', () => {
9+
const { itemRef, listRef, orderedListRef } = setupRefs()
10+
11+
it('binds a collection as an array', async () => {
12+
const wrapper = mount(
13+
{
14+
template: 'no',
15+
setup() {
16+
const list = useCollection(orderedListRef)
17+
18+
return { list }
19+
},
20+
}
21+
// should work without the plugin
22+
// { global: { plugins: [firestorePlugin] } }
23+
)
24+
25+
expect(wrapper.vm.list).toEqual([])
26+
await usePendingPromises()
27+
28+
await addDoc(listRef, { name: 'a' })
29+
await addDoc(listRef, { name: 'b' })
30+
await addDoc(listRef, { name: 'c' })
31+
expect(wrapper.vm.list).toHaveLength(3)
32+
expect(wrapper.vm.list).toEqual([
33+
{ name: 'a' },
34+
{ name: 'b' },
35+
{ name: 'c' },
36+
])
37+
})
38+
})

Diff for: tests/utils.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { initializeApp } from 'firebase/app'
2+
import {
3+
getFirestore,
4+
connectFirestoreEmulator,
5+
collection,
6+
doc,
7+
query,
8+
orderBy,
9+
CollectionReference,
10+
getDocsFromServer,
11+
QueryDocumentSnapshot,
12+
deleteDoc,
13+
} from 'firebase/firestore'
14+
import { afterAll } from 'vitest'
15+
import { isCollectionRef, isDocumentRef } from '../src/shared'
16+
17+
const firebaseApp = initializeApp({ projectId: 'vue-fire-store' })
18+
const firestore = getFirestore(firebaseApp)
19+
connectFirestoreEmulator(firestore, 'localhost', 8080)
20+
21+
let _id = 0
22+
export function setupRefs() {
23+
const testId = _id++
24+
const testsCollection = collection(firestore, `__tests`)
25+
const itemRef = doc(testsCollection, `item:${testId}`)
26+
const forItemsRef = doc(testsCollection, `forItems:${testId}`)
27+
28+
const listRef = collection(forItemsRef, 'list')
29+
const orderedListRef = query(listRef, orderBy('name'))
30+
31+
afterAll(async () => {
32+
// clean up the tests data
33+
await Promise.all([
34+
deleteDoc(itemRef),
35+
clearCollection(listRef),
36+
clearCollection(testsCollection),
37+
])
38+
})
39+
40+
return { itemRef, listRef, orderedListRef, testId, col: forItemsRef }
41+
}
42+
43+
export async function clearCollection(collection: CollectionReference) {
44+
const { docs } = await getDocsFromServer(collection)
45+
await Promise.all(
46+
docs.map(doc => {
47+
return recursiveDeleteDoc(doc)
48+
})
49+
)
50+
}
51+
52+
export async function recursiveDeleteDoc(doc: QueryDocumentSnapshot) {
53+
const docData = doc.data()
54+
const promises: Promise<any>[] = []
55+
if (docData) {
56+
for (const key in docData) {
57+
if (isCollectionRef(docData[key])) {
58+
promises.push(clearCollection(docData[key]))
59+
} else if (isDocumentRef(docData[key])) {
60+
promises.push(recursiveDeleteDoc(docData[key]))
61+
}
62+
}
63+
}
64+
promises.push(deleteDoc(doc.ref))
65+
return Promise.all(promises)
66+
}
67+
68+
export const sleep = (ms: number) =>
69+
new Promise(resolve => setTimeout(resolve, ms))

0 commit comments

Comments
 (0)