Skip to content

Commit

Permalink
feat(database): useList for arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Oct 17, 2022
1 parent 3b376f4 commit 86ccfc7
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 27 deletions.
15 changes: 15 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
DocumentData,
DocumentReference,
} from 'firebase/firestore'
import type { Ref } from 'vue-demi'

// FIXME: replace any with unknown or T generics

Expand Down Expand Up @@ -111,3 +112,17 @@ export function callOnceWithArg<T, K>(
}
}
}

/**
* @internal
*/
export interface _RefWithState<T> extends Ref<T> {
get data(): Ref<T>
get error(): Ref<Error | undefined>
get pending(): Ref<boolean>

// TODO: is it really void?
get promise(): Promise<void>
// TODO: extract type from bindDocument and bindCollection
unbind: () => void
}
15 changes: 1 addition & 14 deletions src/vuefire/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
toRef,
InjectionKey,
} from 'vue-demi'
import { _RefWithState } from '../shared'

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

/**
* @internal
*/
export interface _RefWithState<T> extends Ref<T> {
get data(): Ref<T>
get error(): Ref<Error | undefined>
get pending(): Ref<boolean>

// TODO: is it really void?
promise: Promise<void>
// TODO: extract type from bindDocument and bindCollection
unbind: () => void
}

export const unbind = (target: Ref, reset?: FirestoreOptions['reset']) =>
internalUnbind('', firestoreUnbinds.get(target), reset)

Expand Down
7 changes: 6 additions & 1 deletion src/vuefire/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { rtdbPlugin, bind as rtdbBind, unbind as rtdbUnbind } from './rtdb'
export {
rtdbPlugin,
bind as rtdbBind,
unbind as rtdbUnbind,
useList,
} from './rtdb'
export {
firestorePlugin,
bind as firestoreBind,
Expand Down
57 changes: 56 additions & 1 deletion src/vuefire/rtdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ import {
getCurrentInstance,
onBeforeUnmount,
isVue3,
ref,
getCurrentScope,
onScopeDispose,
} from 'vue-demi'
import type { DatabaseReference, DataSnapshot, Query } from 'firebase/database'
import { _RefWithState } from '../shared'

/**
* Returns the original reference of a Firebase reference or query across SDK versions.
Expand Down Expand Up @@ -158,7 +162,7 @@ const rtdbUnbinds = new WeakMap<
* @param app
* @param pluginOptions
*/
export const rtdbPlugin = function rtdbPlugin(
export function rtdbPlugin(
app: App,
pluginOptions: PluginOptions = defaultOptions
) {
Expand Down Expand Up @@ -274,5 +278,56 @@ export function bind(
return promise
}

// export function useList(reference: DatabaseReference | Query, options?: RTDBOptions)

/**
* Creates a reactive variable connected to the database.
*
* @param reference - Reference or query to the database
* @param options - optional options
*/
export function useList<T = unknown>(
reference: DatabaseReference | Query,
options?: RTDBOptions
): _RefWithState<T[]> {
const unbinds = {}
const data = ref<T[]>([]) as Ref<T[]>
const error = ref<Error>()
const pending = ref(true)

rtdbUnbinds.set(data, unbinds)
const promise = internalBind(data, '', reference, unbinds, options)
promise
.catch(reason => {
error.value = reason
})
.finally(() => {
pending.value = false
})

// TODO: SSR serialize the values for Nuxt to expose them later and use them
// as initial values while specifying a wait: true to only swap objects once
// Firebase has done its initial sync. Also, on server, you don't need to
// create sync, you can read only once the whole thing so maybe internalBind
// should take an option like once: true to not setting up any listener

if (getCurrentScope()) {
onScopeDispose(() => {
unbind(data, options && options.reset)
})
}

return Object.defineProperties<_RefWithState<T[]>>(
data as _RefWithState<T[]>,
{
data: { get: () => data },
error: { get: () => error },
pending: { get: () => error },

promise: { get: () => promise },
}
)
}

export const unbind = (target: Ref, reset?: RTDBOptions['reset']) =>
internalUnbind('', rtdbUnbinds.get(target), reset)
44 changes: 44 additions & 0 deletions tests/database/list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { useList } from '../../src'
import { expectType, tds, setupDatabaseRefs, database } from '../utils'
import { type Ref } from 'vue'
import { push, ref as _databaseRef, remove } from 'firebase/database'

describe('Database lists', () => {
const { itemRef, listRef, orderedListRef, databaseRef } = setupDatabaseRefs()

it('binds a list', async () => {
const wrapper = mount(
{
template: 'no',
setup() {
const list = useList(orderedListRef)

return { list }
},
}
// should work without the plugin
// { global: { plugins: [firestorePlugin] } }
)

expect(wrapper.vm.list).toEqual([])

await push(listRef, { name: 'a' })
await push(listRef, { name: 'b' })
await push(listRef, { name: 'c' })
expect(wrapper.vm.list).toHaveLength(3)
expect(wrapper.vm.list).toEqual([
{ name: 'a' },
{ name: 'b' },
{ name: 'c' },
])
})

tds(() => {
const db = database
const databaseRef = _databaseRef
expectType<Ref<unknown[]>>(useList(databaseRef(db, 'todos')))
expectType<Ref<number[]>>(useList<number>(databaseRef(db, 'todos')))
})
})
4 changes: 2 additions & 2 deletions tests/firestore/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { useCollection } from '../../src'
import { addDoc, collection, DocumentData } from 'firebase/firestore'
import { expectType, setupRefs, tds, firestore } from '../utils'
import { expectType, setupFirestoreRefs, tds, firestore } from '../utils'
import { usePendingPromises } from '../../src/vuefire/firestore'
import { type Ref } from 'vue'

describe('Firestore collections', () => {
const { itemRef, listRef, orderedListRef } = setupRefs()
const { itemRef, listRef, orderedListRef } = setupFirestoreRefs()

it('binds a collection as an array', async () => {
const wrapper = mount(
Expand Down
4 changes: 2 additions & 2 deletions tests/firestore/document.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import {
setDoc,
updateDoc,
} from 'firebase/firestore'
import { expectType, setupRefs, tds, firestore } from '../utils'
import { expectType, setupFirestoreRefs, tds, firestore } from '../utils'
import { usePendingPromises } from '../../src/vuefire/firestore'
import { type Ref } from 'vue'

describe('Firestore collections', () => {
const { itemRef, listRef, orderedListRef } = setupRefs()
const { itemRef, listRef, orderedListRef } = setupFirestoreRefs()

it('binds a collection as an array', async () => {
const wrapper = mount(
Expand Down
49 changes: 42 additions & 7 deletions tests/utils.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
import { initializeApp } from 'firebase/app'
import {
connectDatabaseEmulator,
getDatabase,
ref,
query as databaseQuery,
orderByChild,
remove,
} from 'firebase/database'
import {
getFirestore,
connectFirestoreEmulator,
collection,
doc,
query,
query as firestoreQuery,
orderBy,
CollectionReference,
getDocsFromServer,
QueryDocumentSnapshot,
deleteDoc,
} from 'firebase/firestore'
import { afterAll } from 'vitest'
import { beforeAll } from 'vitest'
import { isCollectionRef, isDocumentRef } from '../src/shared'

export const firebaseApp = initializeApp({ projectId: 'vue-fire-store' })
export const firestore = getFirestore(firebaseApp)
export const database = getDatabase(firebaseApp)

connectFirestoreEmulator(firestore, 'localhost', 8080)
connectDatabaseEmulator(database, 'localhost', 8081)

let _id = 0
export function setupRefs() {

// Firestore
export function setupFirestoreRefs() {
const testId = _id++
const testsCollection = collection(firestore, `__tests`)
const itemRef = doc(testsCollection, `item:${testId}`)
const forItemsRef = doc(testsCollection, `forItems:${testId}`)

const listRef = collection(forItemsRef, 'list')
const orderedListRef = query(listRef, orderBy('name'))
const orderedListRef = firestoreQuery(listRef, orderBy('name'))

afterAll(async () => {
beforeAll(async () => {
// clean up the tests data
await Promise.all([
deleteDoc(itemRef),
Expand All @@ -40,7 +53,7 @@ export function setupRefs() {
return { itemRef, listRef, orderedListRef, testId, col: forItemsRef }
}

export async function clearCollection(collection: CollectionReference) {
async function clearCollection(collection: CollectionReference) {
const { docs } = await getDocsFromServer(collection)
await Promise.all(
docs.map(doc => {
Expand All @@ -49,7 +62,7 @@ export async function clearCollection(collection: CollectionReference) {
)
}

export async function recursiveDeleteDoc(doc: QueryDocumentSnapshot) {
async function recursiveDeleteDoc(doc: QueryDocumentSnapshot) {
const docData = doc.data()
const promises: Promise<any>[] = []
if (docData) {
Expand All @@ -65,6 +78,28 @@ export async function recursiveDeleteDoc(doc: QueryDocumentSnapshot) {
return Promise.all(promises)
}

// Database
export function setupDatabaseRefs() {
const testId = _id++
const testsCollection = ref(database, `__tests_${testId}`)

const itemRef = ref(database, testsCollection.key + `/item`)
const listRef = ref(database, testsCollection.key + `/items`)
const orderedListRef = databaseQuery(listRef, orderByChild('name'))

beforeAll(async () => {
// clean up the tests data
await remove(testsCollection)
})

function databaseRef(path: string) {
return ref(database, testsCollection.key + '/' + path)
}

return { itemRef, listRef, orderedListRef, testId, databaseRef }
}

// General utils
export const sleep = (ms: number) =>
new Promise(resolve => setTimeout(resolve, ms))

Expand Down

0 comments on commit 86ccfc7

Please sign in to comment.