Skip to content

Commit 18224e4

Browse files
committed
feat(firestore): allow custom converter
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.
1 parent 1c8012e commit 18224e4

File tree

5 files changed

+84
-48
lines changed

5 files changed

+84
-48
lines changed

src/firestore/index.ts

+22-13
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ import { onSnapshot } from 'firebase/firestore'
2020
export interface FirestoreOptions {
2121
maxRefDepth?: number
2222
reset?: boolean | (() => any)
23-
/**
24-
* @deprecated use `converter` instead
25-
*/
26-
serialize?: FirestoreSerializer
2723

2824
converter?: FirestoreDataConverter<unknown>
2925

@@ -33,7 +29,6 @@ export interface FirestoreOptions {
3329
const DEFAULT_OPTIONS: Required<FirestoreOptions> = {
3430
maxRefDepth: 2,
3531
reset: true,
36-
serialize: createSnapshot,
3732
converter: firestoreDefaultConverter,
3833
wait: false,
3934
}
@@ -65,7 +60,9 @@ function updateDataFromDocumentSnapshot<T>(
6560
resolve: CommonBindOptionsParameter['resolve']
6661
) {
6762
const [data, refs] = extractRefs(
68-
options.serialize(snapshot),
63+
// @ts-expect-error: FIXME: use better types
64+
// Pass snapshot options
65+
snapshot.data(),
6966
walkGet(target, path),
7067
subs
7168
)
@@ -200,7 +197,7 @@ interface BindCollectionParameter extends CommonBindOptionsParameter {
200197
collection: CollectionReference | Query
201198
}
202199

203-
export function bindCollection<T>(
200+
export function bindCollection<T = unknown>(
204201
target: BindCollectionParameter['target'],
205202
collection: CollectionReference<T> | Query<T>,
206203
ops: BindCollectionParameter['ops'],
@@ -209,11 +206,15 @@ export function bindCollection<T>(
209206
extraOptions: FirestoreOptions = DEFAULT_OPTIONS
210207
) {
211208
const options = Object.assign({}, DEFAULT_OPTIONS, extraOptions) // fill default values
212-
// a custom converter means we don't need a serializer
213-
if (collection.converter) {
214-
// @ts-expect-error: FIXME: remove this serialize option
215-
options.serialize = (v) => v.data()
209+
210+
if (!collection.converter) {
211+
// @ts-expect-error: seems like a ts error
212+
collection = collection.withConverter(
213+
// @ts-expect-error: seems like a ts error
214+
options.converter as FirestoreDataConverter<T>
215+
)
216216
}
217+
217218
const key = 'value'
218219
if (!options.wait) ops.set(target, key, [])
219220
let arrayRef = ref(options.wait ? [] : target[key])
@@ -228,7 +229,13 @@ export function bindCollection<T>(
228229
added: ({ newIndex, doc }: DocumentChange<T>) => {
229230
arraySubs.splice(newIndex, 0, Object.create(null))
230231
const subs = arraySubs[newIndex]
231-
const [data, refs] = extractRefs(options.serialize(doc), undefined, subs)
232+
// FIXME: wrong cast, needs better types
233+
// TODO: pass SnapshotOptions
234+
const [data, refs] = extractRefs(
235+
doc.data() as DocumentData,
236+
undefined,
237+
subs
238+
)
232239
ops.add(unref(arrayRef), newIndex, data)
233240
subscribeToRefs(
234241
options,
@@ -245,7 +252,9 @@ export function bindCollection<T>(
245252
const array = unref(arrayRef)
246253
const subs = arraySubs[oldIndex]
247254
const oldData = array[oldIndex]
248-
const [data, refs] = extractRefs(options.serialize(doc), oldData, subs)
255+
// @ts-expect-error: FIXME: Better types
256+
// TODO: pass SnapshotOptions
257+
const [data, refs] = extractRefs(doc.data(), oldData, subs)
249258
// only move things around after extracting refs
250259
// only move things around after extracting refs
251260
arraySubs.splice(newIndex, 0, subs)

src/firestore/utils.ts

+23-21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
FirestoreDataConverter,
1010
} from 'firebase/firestore'
1111
import { isTimestamp, isObject, isDocumentRef, TODO } from '../shared'
12+
import { VueFireDocumentData } from '../vuefire/firestore'
1213

1314
export type FirestoreReference = Query | DocumentReference | CollectionReference
1415

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

26-
export const firestoreDefaultConverter: FirestoreDataConverter<unknown> = {
27-
toFirestore(data) {
28-
// this is okay because we declare other properties as non-enumerable
29-
return data as DocumentData
30-
},
31-
fromFirestore(snapshot, options) {
32-
return snapshot.exists()
33-
? Object.defineProperties(snapshot.data(options)!, {
34-
id: {
35-
// TODO: can the `id` change? If so this should be a get
36-
value: () => snapshot.id,
37-
},
38-
// TODO: check if worth adding or should be through an option
39-
// $meta: {
40-
// value: snapshot.metadata,
41-
// },
42-
// $ref: { get: () => snapshot.ref },
43-
})
44-
: null
45-
},
46-
}
27+
export const firestoreDefaultConverter: FirestoreDataConverter<VueFireDocumentData> =
28+
{
29+
toFirestore(data) {
30+
// this is okay because we declare other properties as non-enumerable
31+
return data as DocumentData
32+
},
33+
fromFirestore(snapshot, options) {
34+
return snapshot.exists()
35+
? (Object.defineProperties(snapshot.data(options)!, {
36+
id: {
37+
// TODO: can the `id` change? If so this should be a get
38+
value: () => snapshot.id,
39+
},
40+
// TODO: check if worth adding or should be through an option
41+
// $meta: {
42+
// value: snapshot.metadata,
43+
// },
44+
// $ref: { get: () => snapshot.ref },
45+
}) as VueFireDocumentData)
46+
: null
47+
},
48+
}
4749

4850
export type FirestoreSerializer = typeof createSnapshot
4951

src/vuefire/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ export {
1212
useDocument,
1313
} from './firestore'
1414

15-
export type { UseCollectionOptions } from './firestore'
15+
export type {
16+
UseCollectionOptions,
17+
VueFireDocumentData,
18+
VueFireQueryData,
19+
} from './firestore'
1620

1721
export { firestorePlugin } from './optionsApi'
1822
export type {

src/vuefire/optionsApi.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,15 @@ export const firestoreUnbinds = new WeakMap<
8282
export interface PluginOptions {
8383
bindName?: string
8484
unbindName?: string
85-
serialize?: FirestoreOptions['serialize']
85+
converter?: FirestoreOptions['converter']
8686
reset?: FirestoreOptions['reset']
8787
wait?: FirestoreOptions['wait']
8888
}
8989

9090
const defaultOptions: Readonly<Required<PluginOptions>> = {
9191
bindName: '$bind',
9292
unbindName: '$unbind',
93-
serialize: firestoreOptions.serialize,
93+
converter: firestoreOptions.converter,
9494
reset: firestoreOptions.reset,
9595
wait: firestoreOptions.wait,
9696
}

tests/firestore/options.spec.ts

+32-11
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@ describe('Firestore: Options API', () => {
3535

3636
it('calls custom serialize function with collection', async () => {
3737
const pluginOptions: PluginOptions = {
38-
// @ts-expect-error: FIXME:
39-
serialize: vi.fn(() => ({ foo: 'bar' })),
38+
converter: {
39+
fromFirestore: vi.fn((snapshot, options?) => ({
40+
foo: 'bar',
41+
})),
42+
toFirestore(data: DocumentData) {
43+
return data
44+
},
45+
},
4046
}
4147
const wrapper = mount(
4248
{
@@ -55,16 +61,24 @@ describe('Firestore: Options API', () => {
5561

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

58-
expect(pluginOptions.serialize).toHaveBeenCalledTimes(1)
59-
expect(pluginOptions.serialize).toHaveBeenCalledWith(
60-
expect.objectContaining({ data: expect.any(Function) })
64+
expect(pluginOptions.converter?.fromFirestore).toHaveBeenCalledTimes(1)
65+
expect(pluginOptions.converter?.fromFirestore).toHaveBeenCalledWith(
66+
expect.objectContaining({ data: expect.any(Function) }),
67+
expect.anything()
6168
)
6269
expect(wrapper.vm.items).toEqual([{ foo: 'bar' }])
6370
})
6471

6572
it('can be overridden by local option', async () => {
66-
const pluginOptions = {
67-
serialize: vi.fn(() => ({ foo: 'bar' })),
73+
const pluginOptions: PluginOptions = {
74+
converter: {
75+
fromFirestore: vi.fn((snapshot, options?) => ({
76+
foo: 'bar',
77+
})),
78+
toFirestore(data: DocumentData) {
79+
return data
80+
},
81+
},
6882
}
6983
const wrapper = mount(
7084
{
@@ -83,13 +97,20 @@ describe('Firestore: Options API', () => {
8397

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

86-
// @ts-expect-error: FIXME:
87-
await wrapper.vm.$bind('items', itemsRef, { serialize: spy })
100+
await wrapper.vm.$bind('items', itemsRef, {
101+
converter: {
102+
fromFirestore: spy,
103+
toFirestore(data: DocumentData) {
104+
return data
105+
},
106+
},
107+
})
88108

89-
expect(pluginOptions.serialize).not.toHaveBeenCalled()
109+
expect(pluginOptions.converter?.fromFirestore).not.toHaveBeenCalled()
90110
expect(spy).toHaveBeenCalledTimes(1)
91111
expect(spy).toHaveBeenCalledWith(
92-
expect.objectContaining({ data: expect.any(Function) })
112+
expect.objectContaining({ data: expect.any(Function) }),
113+
expect.anything()
93114
)
94115
expect(wrapper.vm.items).toEqual([{ bar: 'bar' }])
95116
})

0 commit comments

Comments
 (0)