Skip to content

Commit 795c500

Browse files
committed
fix(database, firestore): avoid id field name conflict (vuejs#1472)
1 parent 64308fb commit 795c500

File tree

9 files changed

+78
-48
lines changed

9 files changed

+78
-48
lines changed

src/database/utils.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,21 @@ export function createRecordFromDatabaseSnapshot(
1515

1616
const value: unknown = snapshot.val()
1717
return isObject(value)
18-
? (Object.defineProperty(value, 'id', {
19-
// allow destructuring without interfering without using the `id` property
20-
value: snapshot.key,
18+
? (Object.defineProperties(value, {
19+
// allow destructuring without interfering without using the `.key` property
20+
'.key': { value: snapshot.key },
21+
'.priority': { value: snapshot.priority },
22+
'.ref': { value: snapshot.ref },
23+
'.size': { value: snapshot.size },
2124
}) as VueDatabaseDocumentData<unknown>)
2225
: {
2326
// if the value is a primitive we can just return a regular object, it's easier to debug
2427
// @ts-expect-error: $value doesn't exist
2528
$value: value,
26-
id: snapshot.key,
29+
'.key': snapshot.key,
30+
'.priority': snapshot.priority,
31+
'.ref': snapshot.ref,
32+
'.size': snapshot.size,
2733
}
2834
}
2935

@@ -43,7 +49,7 @@ export function indexForKey(
4349
key: string | null | number
4450
): number {
4551
for (let i = 0; i < array.length; i++) {
46-
if (array[i].id === key) return i
52+
if (array[i]['.key'] === key) return i
4753
}
4854

4955
return -1
@@ -58,9 +64,25 @@ export type VueDatabaseDocumentData<T = unknown> =
5864
| null
5965
| (T & {
6066
/**
61-
* id of the document
67+
* The key (last part of the path) of the location of this DataSnapshot.The last token in a Database location is
68+
* considered its key. For example, "ada" is the key for the /users/ada/ node. Accessing the key on any
69+
* DataSnapshot will return the key for the location that generated it. However, accessing the key on the root URL
70+
* of a Database will return null.
6271
*/
63-
readonly id: string
72+
readonly '.key': string
73+
/**
74+
* The priority value of the data in this DataSnapshot.Applications need not use priority but can order
75+
* collections by ordinary properties.
76+
*/
77+
readonly '.priority': string
78+
/**
79+
* The location of this document data.
80+
*/
81+
readonly '.ref': string
82+
/**
83+
* The number of child properties of this document data.
84+
*/
85+
readonly '.size': string
6486
})
6587

6688
/**

src/firestore/useFirestoreRef.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,15 @@ export type VueFirestoreDocumentData<T = DocumentData> =
229229
/**
230230
* id of the document
231231
*/
232-
readonly id: string
232+
readonly '.id': string
233+
/**
234+
* Metadata about the DocumentSnapshot, including information about its source and local modifications.
235+
*/
236+
readonly '.metadata': string
237+
/**
238+
* The DocumentReference for the document
239+
*/
240+
readonly '.ref': string
233241
})
234242

235243
export type VueFirestoreQueryData<T = DocumentData> = Array<

src/firestore/utils.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ export const firestoreDefaultConverter: FirestoreDataConverter<VueFirestoreDocum
2424
return data as DocumentData
2525
},
2626
fromFirestore(snapshot, options) {
27-
return snapshot.exists()
28-
? (Object.defineProperties(snapshot.data(options)!, {
29-
id: { value: snapshot.id },
30-
// TODO: check if worth adding or should be through an option
31-
// It could also be an example in the docs about converters
32-
// $meta: {
33-
// value: snapshot.metadata,
34-
// },
35-
// $ref: { get: () => snapshot.ref },
36-
}) as VueFirestoreDocumentData)
37-
: null
27+
if (snapshot.exists()) {
28+
const data = snapshot.data(options)!
29+
Object.defineProperties(data, {
30+
'.id': { value: snapshot.id },
31+
'.metadata': { value: snapshot.metadata },
32+
'.ref': { value: snapshot.ref },
33+
})
34+
return data as VueFirestoreDocumentData
35+
} else {
36+
return null
37+
}
3838
},
3939
}
4040

tests/database/list.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ describe('Database lists', () => {
9797
await promise.value
9898

9999
expect(wrapper.vm.list).toMatchObject([
100-
{ $value: 'a', id: expect.any(String) },
101-
{ $value: 'b', id: expect.any(String) },
102-
{ $value: 'c', id: expect.any(String) },
100+
{ $value: 'a', '.key': expect.any(String) },
101+
{ $value: 'b', '.key': expect.any(String) },
102+
{ $value: 'c', '.key': expect.any(String) },
103103
])
104104
})
105105

@@ -169,8 +169,8 @@ describe('Database lists', () => {
169169

170170
const a = await push(listRef, { name: 'a' })
171171
expect(wrapper.vm.list).toHaveLength(1)
172-
expect(data.value[0].id).toBeTypeOf('string')
173-
expect(data.value[0].id).toEqual(a.key)
172+
expect(data.value[0]['.key']).toBeTypeOf('string')
173+
expect(data.value[0]['.key']).toEqual(a.key)
174174
})
175175

176176
it('unbinds when the component is unmounted', async () => {
@@ -397,7 +397,7 @@ describe('Database lists', () => {
397397
useDatabaseList(databaseRef(db, 'todos'))
398398
)
399399
expectType<string | undefined>(
400-
useDatabaseList(databaseRef(db, 'todos')).value?.[0]?.id
400+
useDatabaseList(databaseRef(db, 'todos')).value?.[0]?.['.key']
401401
)
402402
expectType<Ref<VueDatabaseQueryData<number>>>(
403403
useDatabaseList<number>(databaseRef(db, 'todos'))

tests/database/objects.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ describe('Database objects', () => {
147147

148148
expect(wrapper.vm.item).toMatchObject({
149149
$value: 24,
150-
id: itemRef.key,
150+
'.key': itemRef.key,
151151
})
152152
})
153153

@@ -294,7 +294,7 @@ describe('Database objects', () => {
294294
useDatabaseObject<{ name: string }>(databaseRef(db, 'todo'))
295295
)
296296
expectType<undefined | string>(
297-
useDatabaseObject(databaseRef(db, 'todo')).value?.id
297+
useDatabaseObject(databaseRef(db, 'todo')).value?.['.key']
298298
)
299299
expectType<Ref<number | null | undefined>>(
300300
useDatabaseObject<number>(databaseRef(db, 'todo'))

tests/firestore/collection.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ describe(
229229

230230
const a = await addDoc(listRef, { name: 'a' })
231231
expect(wrapper.vm.list).toHaveLength(1)
232-
expect(data.value[0].id).toBeTypeOf('string')
233-
expect(data.value[0].id).toEqual(a.id)
232+
expect(data.value[0]['.id']).toBeTypeOf('string')
233+
expect(data.value[0]['.id']).toEqual(a.id)
234234
})
235235

236236
it('unbinds when the component is unbound', async () => {
@@ -472,13 +472,14 @@ describe(
472472

473473
// Adds the id
474474
// FIXME: this one is any but the test passes
475-
expectType<string>(useCollection(collection(db, 'todos')).value[0].id)
475+
expectType<string>(useCollection(collection(db, 'todos')).value[0]['.id'])
476476
expectType<string>(
477-
useCollection<TodoI>(collection(db, 'todos')).value[0].id
477+
useCollection<TodoI>(collection(db, 'todos')).value[0]['.id']
478478
)
479479
expectType<string>(
480-
useCollection<unknown>(collection(db, 'todos')).value[0].id
480+
useCollection<unknown>(collection(db, 'todos')).value[0]['.id']
481481
)
482+
// @ts-expect-error: no id with custom converter
482483
useCollection(
483484
collection(db, 'todos').withConverter<TodoI, DocumentData>({
484485
fromFirestore: (snapshot) => {
@@ -487,18 +488,17 @@ describe(
487488
},
488489
toFirestore: (todo) => todo,
489490
})
490-
// @ts-expect-error: no id with custom converter
491-
).value[0].id
491+
).value[0]['.id']
492492

493493
expectType<Ref<TodoI[]>>(useCollection<TodoI>(collection(db, 'todos')))
494494
expectType<Ref<TodoI[]>>(
495495
useCollection<TodoI>(collection(db, 'todos')).data
496496
)
497497
expectType<string>(
498-
useCollection<TodoI>(collection(db, 'todos')).value.at(0)!.id
498+
useCollection<TodoI>(collection(db, 'todos')).value.at(0)!['.id']
499499
)
500500
expectType<string>(
501-
useCollection<TodoI>(collection(db, 'todos')).data.value.at(0)!.id
501+
useCollection<TodoI>(collection(db, 'todos')).data.value.at(0)!['.id']
502502
)
503503
// @ts-expect-error: wrong type
504504
expectType<Ref<string[]>>(useCollection<TodoI>(collection(db, 'todos')))
@@ -513,7 +513,7 @@ describe(
513513
expectType<Ref<number[]>>(useCollection(refWithConverter))
514514
expectType<Ref<number[]>>(useCollection(refWithConverter).data)
515515
// @ts-expect-error: no id with converter
516-
expectType<Ref<number[]>>(useCollection(refWithConverter).data.value.id)
516+
expectType<Ref<number[]>>(useCollection(refWithConverter).data.value['.id'])
517517
// @ts-expect-error
518518
expectType<Ref<string[]>>(useCollection(refWithConverter))
519519
})

tests/firestore/document.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ describe(
140140
const { itemRef, data } = factory()
141141

142142
await setDoc(itemRef, { name: 'a' })
143-
expect(data.value!.id).toBe(itemRef.id)
143+
expect(data.value!['.id']).toBe(itemRef.id)
144144
})
145145

146146
it('sets pending while loading', async () => {
@@ -347,12 +347,13 @@ describe(
347347

348348
// Adds the id
349349
// FIXME: this one is any but the test passes
350-
expectType<string>(useDocument(doc(db, 'todos', '1')).value?.id)
351-
expectType<string>(useDocument<TodoI>(doc(db, 'todos', '1')).value!.id)
350+
expectType<string>(useDocument(doc(db, 'todos', '1')).value?.['.id'])
351+
expectType<string>(useDocument<TodoI>(doc(db, 'todos', '1')).value!['.id'])
352352
expectType<_Nullable<TodoI>>(
353353
useDocument<TodoI>(doc(db, 'todos', '1')).value
354354
)
355-
expectType<string>(useDocument<unknown>(doc(db, 'todos', '1')).value!.id)
355+
expectType<string>(useDocument<unknown>(doc(db, 'todos', '1')).value!['.id'])
356+
// @ts-expect-error: no id with custom converter
356357
useDocument(
357358
doc(db, 'todos').withConverter<TodoI, DocumentData>({
358359
fromFirestore: (snapshot) => {
@@ -361,8 +362,7 @@ describe(
361362
},
362363
toFirestore: (todo) => todo,
363364
})
364-
// @ts-expect-error: no id with custom converter
365-
).value?.id
365+
).value?.['.id']
366366

367367
expectType<Ref<number | null | undefined>>(useDocument<number>(itemRef))
368368
expectType<Ref<number | null | undefined>>(
@@ -386,7 +386,7 @@ describe(
386386
// @ts-expect-error: string is not assignable to number
387387
expectType<Ref<string>>(useDocument(refWithConverter))
388388
// @ts-expect-error: no id when a custom converter is used
389-
useDocument(refWithConverter).value.id
389+
useDocument(refWithConverter).value['.id']
390390

391391
// destructuring
392392
expectType<Ref<DocumentData | null | undefined>>(

tests/firestore/refs-in-documents.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ describe('Firestore refs in documents', async () => {
120120
await promise.value
121121
// NOTE: why does toEqual fail here?
122122
expect(data.value).toMatchObject({
123-
id: docRef.id,
124-
a: { name: 'a', id: aRef.id },
123+
'.id': docRef.id,
124+
a: { name: 'a', '.id': aRef.id },
125125
})
126126
})
127127

tests/firestore/utils.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ describe('Firestore and Database utils', () => {
3030

3131
it('createSnapshot adds an id', async () => {
3232
const snapshot = await addDocToCollection()
33-
expect(snapshot.data()?.id).not.toBeFalsy()
33+
expect(snapshot.data()?.['.id']).not.toBeFalsy()
3434
})
3535

3636
it('id is not enumerable', async () => {
3737
const snapshot = await addDocToCollection()
38-
expect(Object.keys(snapshot.data() ?? {}).includes('id')).toBe(false)
38+
expect(Object.keys(snapshot.data() ?? {}).includes('.id')).toBe(false)
3939
})
4040

4141
it('contains all the data', async () => {

0 commit comments

Comments
 (0)