Skip to content

Commit

Permalink
feat(firestore): allow custom converter
Browse files Browse the repository at this point in the history
Close #608

BREAKING CHANGE: `options.serialize()` is replaced with `converter`. It
effectively has the same effect as calling `doc().withConverter()` or
`collection().withConverter()` but it allows to have a global converter
that is automatically applied to all snapshots. This custom converter
adds a non-enumerable `id` property for documents like the previous
`serialize` options. **If you were not using this option**, you don't
need to change anything.
  • Loading branch information
posva committed Oct 19, 2022
1 parent 1c8012e commit 18224e4
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 48 deletions.
35 changes: 22 additions & 13 deletions src/firestore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ import { onSnapshot } from 'firebase/firestore'
export interface FirestoreOptions {
maxRefDepth?: number
reset?: boolean | (() => any)
/**
* @deprecated use `converter` instead
*/
serialize?: FirestoreSerializer

converter?: FirestoreDataConverter<unknown>

Expand All @@ -33,7 +29,6 @@ export interface FirestoreOptions {
const DEFAULT_OPTIONS: Required<FirestoreOptions> = {
maxRefDepth: 2,
reset: true,
serialize: createSnapshot,
converter: firestoreDefaultConverter,
wait: false,
}
Expand Down Expand Up @@ -65,7 +60,9 @@ function updateDataFromDocumentSnapshot<T>(
resolve: CommonBindOptionsParameter['resolve']
) {
const [data, refs] = extractRefs(
options.serialize(snapshot),
// @ts-expect-error: FIXME: use better types
// Pass snapshot options
snapshot.data(),
walkGet(target, path),
subs
)
Expand Down Expand Up @@ -200,7 +197,7 @@ interface BindCollectionParameter extends CommonBindOptionsParameter {
collection: CollectionReference | Query
}

export function bindCollection<T>(
export function bindCollection<T = unknown>(
target: BindCollectionParameter['target'],
collection: CollectionReference<T> | Query<T>,
ops: BindCollectionParameter['ops'],
Expand All @@ -209,11 +206,15 @@ export function bindCollection<T>(
extraOptions: FirestoreOptions = DEFAULT_OPTIONS
) {
const options = Object.assign({}, DEFAULT_OPTIONS, extraOptions) // fill default values
// a custom converter means we don't need a serializer
if (collection.converter) {
// @ts-expect-error: FIXME: remove this serialize option
options.serialize = (v) => v.data()

if (!collection.converter) {
// @ts-expect-error: seems like a ts error
collection = collection.withConverter(
// @ts-expect-error: seems like a ts error
options.converter as FirestoreDataConverter<T>
)
}

const key = 'value'
if (!options.wait) ops.set(target, key, [])
let arrayRef = ref(options.wait ? [] : target[key])
Expand All @@ -228,7 +229,13 @@ export function bindCollection<T>(
added: ({ newIndex, doc }: DocumentChange<T>) => {
arraySubs.splice(newIndex, 0, Object.create(null))
const subs = arraySubs[newIndex]
const [data, refs] = extractRefs(options.serialize(doc), undefined, subs)
// FIXME: wrong cast, needs better types
// TODO: pass SnapshotOptions
const [data, refs] = extractRefs(
doc.data() as DocumentData,
undefined,
subs
)
ops.add(unref(arrayRef), newIndex, data)
subscribeToRefs(
options,
Expand All @@ -245,7 +252,9 @@ export function bindCollection<T>(
const array = unref(arrayRef)
const subs = arraySubs[oldIndex]
const oldData = array[oldIndex]
const [data, refs] = extractRefs(options.serialize(doc), oldData, subs)
// @ts-expect-error: FIXME: Better types
// TODO: pass SnapshotOptions
const [data, refs] = extractRefs(doc.data(), oldData, subs)
// only move things around after extracting refs
// only move things around after extracting refs
arraySubs.splice(newIndex, 0, subs)
Expand Down
44 changes: 23 additions & 21 deletions src/firestore/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
FirestoreDataConverter,
} from 'firebase/firestore'
import { isTimestamp, isObject, isDocumentRef, TODO } from '../shared'
import { VueFireDocumentData } from '../vuefire/firestore'

export type FirestoreReference = Query | DocumentReference | CollectionReference

Expand All @@ -23,27 +24,28 @@ export function createSnapshot<T = DocumentData>(
return Object.defineProperty(doc.data() || {}, 'id', { value: doc.id })
}

export const firestoreDefaultConverter: FirestoreDataConverter<unknown> = {
toFirestore(data) {
// this is okay because we declare other properties as non-enumerable
return data as DocumentData
},
fromFirestore(snapshot, options) {
return snapshot.exists()
? Object.defineProperties(snapshot.data(options)!, {
id: {
// TODO: can the `id` change? If so this should be a get
value: () => snapshot.id,
},
// TODO: check if worth adding or should be through an option
// $meta: {
// value: snapshot.metadata,
// },
// $ref: { get: () => snapshot.ref },
})
: null
},
}
export const firestoreDefaultConverter: FirestoreDataConverter<VueFireDocumentData> =
{
toFirestore(data) {
// this is okay because we declare other properties as non-enumerable
return data as DocumentData
},
fromFirestore(snapshot, options) {
return snapshot.exists()
? (Object.defineProperties(snapshot.data(options)!, {
id: {
// TODO: can the `id` change? If so this should be a get
value: () => snapshot.id,
},
// TODO: check if worth adding or should be through an option
// $meta: {
// value: snapshot.metadata,
// },
// $ref: { get: () => snapshot.ref },
}) as VueFireDocumentData)
: null
},
}

export type FirestoreSerializer = typeof createSnapshot

Expand Down
6 changes: 5 additions & 1 deletion src/vuefire/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ export {
useDocument,
} from './firestore'

export type { UseCollectionOptions } from './firestore'
export type {
UseCollectionOptions,
VueFireDocumentData,
VueFireQueryData,
} from './firestore'

export { firestorePlugin } from './optionsApi'
export type {
Expand Down
4 changes: 2 additions & 2 deletions src/vuefire/optionsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ export const firestoreUnbinds = new WeakMap<
export interface PluginOptions {
bindName?: string
unbindName?: string
serialize?: FirestoreOptions['serialize']
converter?: FirestoreOptions['converter']
reset?: FirestoreOptions['reset']
wait?: FirestoreOptions['wait']
}

const defaultOptions: Readonly<Required<PluginOptions>> = {
bindName: '$bind',
unbindName: '$unbind',
serialize: firestoreOptions.serialize,
converter: firestoreOptions.converter,
reset: firestoreOptions.reset,
wait: firestoreOptions.wait,
}
Expand Down
43 changes: 32 additions & 11 deletions tests/firestore/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ describe('Firestore: Options API', () => {

it('calls custom serialize function with collection', async () => {
const pluginOptions: PluginOptions = {
// @ts-expect-error: FIXME:
serialize: vi.fn(() => ({ foo: 'bar' })),
converter: {
fromFirestore: vi.fn((snapshot, options?) => ({
foo: 'bar',
})),
toFirestore(data: DocumentData) {
return data
},
},
}
const wrapper = mount(
{
Expand All @@ -55,16 +61,24 @@ describe('Firestore: Options API', () => {

await wrapper.vm.$bind('items', itemsRef)

expect(pluginOptions.serialize).toHaveBeenCalledTimes(1)
expect(pluginOptions.serialize).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.any(Function) })
expect(pluginOptions.converter?.fromFirestore).toHaveBeenCalledTimes(1)
expect(pluginOptions.converter?.fromFirestore).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.any(Function) }),
expect.anything()
)
expect(wrapper.vm.items).toEqual([{ foo: 'bar' }])
})

it('can be overridden by local option', async () => {
const pluginOptions = {
serialize: vi.fn(() => ({ foo: 'bar' })),
const pluginOptions: PluginOptions = {
converter: {
fromFirestore: vi.fn((snapshot, options?) => ({
foo: 'bar',
})),
toFirestore(data: DocumentData) {
return data
},
},
}
const wrapper = mount(
{
Expand All @@ -83,13 +97,20 @@ describe('Firestore: Options API', () => {

const spy = vi.fn(() => ({ bar: 'bar' }))

// @ts-expect-error: FIXME:
await wrapper.vm.$bind('items', itemsRef, { serialize: spy })
await wrapper.vm.$bind('items', itemsRef, {
converter: {
fromFirestore: spy,
toFirestore(data: DocumentData) {
return data
},
},
})

expect(pluginOptions.serialize).not.toHaveBeenCalled()
expect(pluginOptions.converter?.fromFirestore).not.toHaveBeenCalled()
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.any(Function) })
expect.objectContaining({ data: expect.any(Function) }),
expect.anything()
)
expect(wrapper.vm.items).toEqual([{ bar: 'bar' }])
})
Expand Down

0 comments on commit 18224e4

Please sign in to comment.