Skip to content

Commit 86ccfc7

Browse files
committed
feat(database): useList for arrays
1 parent 3b376f4 commit 86ccfc7

File tree

8 files changed

+168
-27
lines changed

8 files changed

+168
-27
lines changed

src/shared.ts

+15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
DocumentData,
44
DocumentReference,
55
} from 'firebase/firestore'
6+
import type { Ref } from 'vue-demi'
67

78
// FIXME: replace any with unknown or T generics
89

@@ -111,3 +112,17 @@ export function callOnceWithArg<T, K>(
111112
}
112113
}
113114
}
115+
116+
/**
117+
* @internal
118+
*/
119+
export interface _RefWithState<T> extends Ref<T> {
120+
get data(): Ref<T>
121+
get error(): Ref<Error | undefined>
122+
get pending(): Ref<boolean>
123+
124+
// TODO: is it really void?
125+
get promise(): Promise<void>
126+
// TODO: extract type from bindDocument and bindCollection
127+
unbind: () => void
128+
}

src/vuefire/firestore.ts

+1-14
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
toRef,
2727
InjectionKey,
2828
} from 'vue-demi'
29+
import { _RefWithState } from '../shared'
2930

3031
export const ops: OperationsType = {
3132
set: (target, key, value) => walkSet(target, key, value),
@@ -428,20 +429,6 @@ export function useDocument<T>(
428429
return data as _RefWithState<T>
429430
}
430431

431-
/**
432-
* @internal
433-
*/
434-
export interface _RefWithState<T> extends Ref<T> {
435-
get data(): Ref<T>
436-
get error(): Ref<Error | undefined>
437-
get pending(): Ref<boolean>
438-
439-
// TODO: is it really void?
440-
promise: Promise<void>
441-
// TODO: extract type from bindDocument and bindCollection
442-
unbind: () => void
443-
}
444-
445432
export const unbind = (target: Ref, reset?: FirestoreOptions['reset']) =>
446433
internalUnbind('', firestoreUnbinds.get(target), reset)
447434

src/vuefire/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
export { rtdbPlugin, bind as rtdbBind, unbind as rtdbUnbind } from './rtdb'
1+
export {
2+
rtdbPlugin,
3+
bind as rtdbBind,
4+
unbind as rtdbUnbind,
5+
useList,
6+
} from './rtdb'
27
export {
38
firestorePlugin,
49
bind as firestoreBind,

src/vuefire/rtdb.ts

+56-1
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ import {
1414
getCurrentInstance,
1515
onBeforeUnmount,
1616
isVue3,
17+
ref,
18+
getCurrentScope,
19+
onScopeDispose,
1720
} from 'vue-demi'
1821
import type { DatabaseReference, DataSnapshot, Query } from 'firebase/database'
22+
import { _RefWithState } from '../shared'
1923

2024
/**
2125
* Returns the original reference of a Firebase reference or query across SDK versions.
@@ -158,7 +162,7 @@ const rtdbUnbinds = new WeakMap<
158162
* @param app
159163
* @param pluginOptions
160164
*/
161-
export const rtdbPlugin = function rtdbPlugin(
165+
export function rtdbPlugin(
162166
app: App,
163167
pluginOptions: PluginOptions = defaultOptions
164168
) {
@@ -274,5 +278,56 @@ export function bind(
274278
return promise
275279
}
276280

281+
// export function useList(reference: DatabaseReference | Query, options?: RTDBOptions)
282+
283+
/**
284+
* Creates a reactive variable connected to the database.
285+
*
286+
* @param reference - Reference or query to the database
287+
* @param options - optional options
288+
*/
289+
export function useList<T = unknown>(
290+
reference: DatabaseReference | Query,
291+
options?: RTDBOptions
292+
): _RefWithState<T[]> {
293+
const unbinds = {}
294+
const data = ref<T[]>([]) as Ref<T[]>
295+
const error = ref<Error>()
296+
const pending = ref(true)
297+
298+
rtdbUnbinds.set(data, unbinds)
299+
const promise = internalBind(data, '', reference, unbinds, options)
300+
promise
301+
.catch(reason => {
302+
error.value = reason
303+
})
304+
.finally(() => {
305+
pending.value = false
306+
})
307+
308+
// TODO: SSR serialize the values for Nuxt to expose them later and use them
309+
// as initial values while specifying a wait: true to only swap objects once
310+
// Firebase has done its initial sync. Also, on server, you don't need to
311+
// create sync, you can read only once the whole thing so maybe internalBind
312+
// should take an option like once: true to not setting up any listener
313+
314+
if (getCurrentScope()) {
315+
onScopeDispose(() => {
316+
unbind(data, options && options.reset)
317+
})
318+
}
319+
320+
return Object.defineProperties<_RefWithState<T[]>>(
321+
data as _RefWithState<T[]>,
322+
{
323+
data: { get: () => data },
324+
error: { get: () => error },
325+
pending: { get: () => error },
326+
327+
promise: { get: () => promise },
328+
}
329+
)
330+
}
331+
277332
export const unbind = (target: Ref, reset?: RTDBOptions['reset']) =>
278333
internalUnbind('', rtdbUnbinds.get(target), reset)

tests/database/list.spec.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { mount } from '@vue/test-utils'
2+
import { describe, expect, it } from 'vitest'
3+
import { useList } from '../../src'
4+
import { expectType, tds, setupDatabaseRefs, database } from '../utils'
5+
import { type Ref } from 'vue'
6+
import { push, ref as _databaseRef, remove } from 'firebase/database'
7+
8+
describe('Database lists', () => {
9+
const { itemRef, listRef, orderedListRef, databaseRef } = setupDatabaseRefs()
10+
11+
it('binds a list', async () => {
12+
const wrapper = mount(
13+
{
14+
template: 'no',
15+
setup() {
16+
const list = useList(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+
27+
await push(listRef, { name: 'a' })
28+
await push(listRef, { name: 'b' })
29+
await push(listRef, { name: 'c' })
30+
expect(wrapper.vm.list).toHaveLength(3)
31+
expect(wrapper.vm.list).toEqual([
32+
{ name: 'a' },
33+
{ name: 'b' },
34+
{ name: 'c' },
35+
])
36+
})
37+
38+
tds(() => {
39+
const db = database
40+
const databaseRef = _databaseRef
41+
expectType<Ref<unknown[]>>(useList(databaseRef(db, 'todos')))
42+
expectType<Ref<number[]>>(useList<number>(databaseRef(db, 'todos')))
43+
})
44+
})

tests/firestore/collection.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { mount } from '@vue/test-utils'
22
import { describe, expect, it } from 'vitest'
33
import { useCollection } from '../../src'
44
import { addDoc, collection, DocumentData } from 'firebase/firestore'
5-
import { expectType, setupRefs, tds, firestore } from '../utils'
5+
import { expectType, setupFirestoreRefs, tds, firestore } from '../utils'
66
import { usePendingPromises } from '../../src/vuefire/firestore'
77
import { type Ref } from 'vue'
88

99
describe('Firestore collections', () => {
10-
const { itemRef, listRef, orderedListRef } = setupRefs()
10+
const { itemRef, listRef, orderedListRef } = setupFirestoreRefs()
1111

1212
it('binds a collection as an array', async () => {
1313
const wrapper = mount(

tests/firestore/document.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import {
99
setDoc,
1010
updateDoc,
1111
} from 'firebase/firestore'
12-
import { expectType, setupRefs, tds, firestore } from '../utils'
12+
import { expectType, setupFirestoreRefs, tds, firestore } from '../utils'
1313
import { usePendingPromises } from '../../src/vuefire/firestore'
1414
import { type Ref } from 'vue'
1515

1616
describe('Firestore collections', () => {
17-
const { itemRef, listRef, orderedListRef } = setupRefs()
17+
const { itemRef, listRef, orderedListRef } = setupFirestoreRefs()
1818

1919
it('binds a collection as an array', async () => {
2020
const wrapper = mount(

tests/utils.ts

+42-7
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,47 @@
11
import { initializeApp } from 'firebase/app'
2+
import {
3+
connectDatabaseEmulator,
4+
getDatabase,
5+
ref,
6+
query as databaseQuery,
7+
orderByChild,
8+
remove,
9+
} from 'firebase/database'
210
import {
311
getFirestore,
412
connectFirestoreEmulator,
513
collection,
614
doc,
7-
query,
15+
query as firestoreQuery,
816
orderBy,
917
CollectionReference,
1018
getDocsFromServer,
1119
QueryDocumentSnapshot,
1220
deleteDoc,
1321
} from 'firebase/firestore'
14-
import { afterAll } from 'vitest'
22+
import { beforeAll } from 'vitest'
1523
import { isCollectionRef, isDocumentRef } from '../src/shared'
1624

1725
export const firebaseApp = initializeApp({ projectId: 'vue-fire-store' })
1826
export const firestore = getFirestore(firebaseApp)
27+
export const database = getDatabase(firebaseApp)
28+
1929
connectFirestoreEmulator(firestore, 'localhost', 8080)
30+
connectDatabaseEmulator(database, 'localhost', 8081)
2031

2132
let _id = 0
22-
export function setupRefs() {
33+
34+
// Firestore
35+
export function setupFirestoreRefs() {
2336
const testId = _id++
2437
const testsCollection = collection(firestore, `__tests`)
2538
const itemRef = doc(testsCollection, `item:${testId}`)
2639
const forItemsRef = doc(testsCollection, `forItems:${testId}`)
2740

2841
const listRef = collection(forItemsRef, 'list')
29-
const orderedListRef = query(listRef, orderBy('name'))
42+
const orderedListRef = firestoreQuery(listRef, orderBy('name'))
3043

31-
afterAll(async () => {
44+
beforeAll(async () => {
3245
// clean up the tests data
3346
await Promise.all([
3447
deleteDoc(itemRef),
@@ -40,7 +53,7 @@ export function setupRefs() {
4053
return { itemRef, listRef, orderedListRef, testId, col: forItemsRef }
4154
}
4255

43-
export async function clearCollection(collection: CollectionReference) {
56+
async function clearCollection(collection: CollectionReference) {
4457
const { docs } = await getDocsFromServer(collection)
4558
await Promise.all(
4659
docs.map(doc => {
@@ -49,7 +62,7 @@ export async function clearCollection(collection: CollectionReference) {
4962
)
5063
}
5164

52-
export async function recursiveDeleteDoc(doc: QueryDocumentSnapshot) {
65+
async function recursiveDeleteDoc(doc: QueryDocumentSnapshot) {
5366
const docData = doc.data()
5467
const promises: Promise<any>[] = []
5568
if (docData) {
@@ -65,6 +78,28 @@ export async function recursiveDeleteDoc(doc: QueryDocumentSnapshot) {
6578
return Promise.all(promises)
6679
}
6780

81+
// Database
82+
export function setupDatabaseRefs() {
83+
const testId = _id++
84+
const testsCollection = ref(database, `__tests_${testId}`)
85+
86+
const itemRef = ref(database, testsCollection.key + `/item`)
87+
const listRef = ref(database, testsCollection.key + `/items`)
88+
const orderedListRef = databaseQuery(listRef, orderByChild('name'))
89+
90+
beforeAll(async () => {
91+
// clean up the tests data
92+
await remove(testsCollection)
93+
})
94+
95+
function databaseRef(path: string) {
96+
return ref(database, testsCollection.key + '/' + path)
97+
}
98+
99+
return { itemRef, listRef, orderedListRef, testId, databaseRef }
100+
}
101+
102+
// General utils
68103
export const sleep = (ms: number) =>
69104
new Promise(resolve => setTimeout(resolve, ms))
70105

0 commit comments

Comments
 (0)