@data-weave/backend-firestore 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/lib/FirestoreDataManager.ts +242 -0
- package/lib/FirestoreList.ts +84 -0
- package/lib/FirestoreMetadata.ts +59 -0
- package/lib/FirestoreReference.ts +75 -0
- package/lib/firestoreTypes.ts +231 -0
- package/lib/index.ts +6 -0
- package/lib/utils.ts +179 -0
- package/lib/version.js +2 -0
- package/package.json +28 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [0.3.3](https://github.com/data-weave/datamanager/compare/v0.3.2...v0.3.3) (2025-09-28)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @data-weave/backend-firestore
|
|
9
|
+
|
|
10
|
+
## [0.3.2](https://github.com/data-weave/datamanager/compare/v0.3.1...v0.3.2) (2025-09-28)
|
|
11
|
+
|
|
12
|
+
**Note:** Version bump only for package @data-weave/backend-firestore
|
|
13
|
+
|
|
14
|
+
## [0.3.1](https://github.com/data-weave/datamanager/compare/v0.3.0...v0.3.1) (2025-09-28)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @data-weave/backend-firestore
|
|
17
|
+
|
|
18
|
+
## 0.3.0 (2025-09-28)
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- add code quality workflow and circular dep check ([9c7b7e3](https://github.com/data-weave/datamanager/commit/9c7b7e34501dfc95ddc2bcff686f55db06d9b1b8))
|
|
23
|
+
- add transactions support ([59db0ff](https://github.com/data-weave/datamanager/commit/59db0ffce889e040828779bd2d03efe5ac088e78))
|
|
24
|
+
- project re-organization ([4a00af3](https://github.com/data-weave/datamanager/commit/4a00af36c4c2f6e49287542e0d5ad85acaa50cda))
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
- adjust config and lib versions ([9c7e2c9](https://github.com/data-weave/datamanager/commit/9c7e2c9072ea34b1e54326c2612b43c92ec2da4e))
|
|
29
|
+
- adjust tests for firestore realtime snapshots ([539f314](https://github.com/data-weave/datamanager/commit/539f31448f68b084dc4e429663fe71e69c98760e))
|
|
30
|
+
- implement Cache prop & clean up some of anys ([955b11a](https://github.com/data-weave/datamanager/commit/955b11a6ab05224f843db834ad28796602679fac))
|
|
31
|
+
|
|
32
|
+
### Reverts
|
|
33
|
+
|
|
34
|
+
- revert incorrect name change ([191e64e](https://github.com/data-weave/datamanager/commit/191e64e1123bdbc3555ff7830a532761157fd8ac))
|
|
35
|
+
|
|
36
|
+
### Code Refactoring
|
|
37
|
+
|
|
38
|
+
- abstract out firestoreReference into LiveReference ([7848a87](https://github.com/data-weave/datamanager/commit/7848a872155593e5acab56696d4d9f27f0e6abb2))
|
|
39
|
+
- abstract out LiveList function from FirestorList ([bb4278e](https://github.com/data-weave/datamanager/commit/bb4278e960d0080d1fb18d847036acb189782ec9))
|
|
40
|
+
- adjustments for LiveList & ListReference ([49cd540](https://github.com/data-weave/datamanager/commit/49cd540295693e40bbb9eb75b5101b45c6932921))
|
|
41
|
+
- include namepspace in DataManager name ([9c0a171](https://github.com/data-weave/datamanager/commit/9c0a1712d8de3cef0d2e1f8022044d0ca3628ae6))
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { injectable } from 'inversify'
|
|
2
|
+
|
|
3
|
+
import { Cache, MapCache } from '@data-weave/datamanager/lib/Cache'
|
|
4
|
+
import { CreateOptions, DataManager, Metadata, WithMetadata } from '@data-weave/datamanager/lib/DataManager'
|
|
5
|
+
import { List, ListPaginationParams } from '@data-weave/datamanager/lib/List'
|
|
6
|
+
import { IdentifiableReference, WithoutId } from '@data-weave/datamanager/lib/Reference'
|
|
7
|
+
import { FirestoreList } from './FirestoreList'
|
|
8
|
+
import {
|
|
9
|
+
FIRESTORE_KEYS,
|
|
10
|
+
FirestoreMetadataConverter,
|
|
11
|
+
FirestoreSerializedMetadata,
|
|
12
|
+
queryNotDeleted,
|
|
13
|
+
} from './FirestoreMetadata'
|
|
14
|
+
import { FirestoreReference } from './FirestoreReference'
|
|
15
|
+
import {
|
|
16
|
+
DocumentData,
|
|
17
|
+
FilterBy,
|
|
18
|
+
FirebaseCreateOptions,
|
|
19
|
+
Firestore,
|
|
20
|
+
FirestoreDataConverter,
|
|
21
|
+
FirestoreReadMode,
|
|
22
|
+
FirestoreReadOptions,
|
|
23
|
+
FirestoreTypes,
|
|
24
|
+
FirestoreWriteOptions,
|
|
25
|
+
InternalFirestoreDataConverter,
|
|
26
|
+
OrderBy,
|
|
27
|
+
WithFieldValue,
|
|
28
|
+
} from './firestoreTypes'
|
|
29
|
+
import { MergeConverters, checkIfReferenceExists } from './utils'
|
|
30
|
+
|
|
31
|
+
export type FirebaseDataManagerDeleteMode = 'soft' | 'hard'
|
|
32
|
+
|
|
33
|
+
export interface FirebaseDataManagerOptions {
|
|
34
|
+
readonly idResolver?: () => string
|
|
35
|
+
readonly deleteMode?: FirebaseDataManagerDeleteMode
|
|
36
|
+
readonly readMode?: FirestoreReadMode
|
|
37
|
+
readonly preventOverwriteOnCreate?: boolean
|
|
38
|
+
// TODO: Add preventUpdateIfNotExists?
|
|
39
|
+
readonly Reference?: typeof FirestoreReference
|
|
40
|
+
readonly List?: typeof FirestoreList
|
|
41
|
+
readonly listCache?: Cache
|
|
42
|
+
readonly refCache?: Cache
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const defaultFirebaseDataManagerOptions: FirebaseDataManagerOptions = {
|
|
46
|
+
deleteMode: 'soft',
|
|
47
|
+
preventOverwriteOnCreate: true,
|
|
48
|
+
readMode: 'static',
|
|
49
|
+
Reference: FirestoreReference,
|
|
50
|
+
List: FirestoreList,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface QueryParams<T extends FirestoreTypes.DocumentData> {
|
|
54
|
+
readonly filters?: Array<FilterBy<T & FirestoreSerializedMetadata>>
|
|
55
|
+
readonly orderBy?: Array<OrderBy<T & FirestoreSerializedMetadata>>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@injectable()
|
|
59
|
+
export class FirestoreDataManager<
|
|
60
|
+
T extends FirestoreTypes.DocumentData,
|
|
61
|
+
SerializedT extends FirestoreTypes.DocumentData,
|
|
62
|
+
> implements DataManager<T>
|
|
63
|
+
{
|
|
64
|
+
private mergedConverter: InternalFirestoreDataConverter<T & Metadata, SerializedT & FirestoreSerializedMetadata>
|
|
65
|
+
private collection: FirestoreTypes.CollectionReference<T & Metadata, SerializedT & FirestoreSerializedMetadata>
|
|
66
|
+
private collectionQuery: FirestoreTypes.Query<T & Metadata, SerializedT & FirestoreSerializedMetadata>
|
|
67
|
+
private managerOptions: FirebaseDataManagerOptions
|
|
68
|
+
|
|
69
|
+
private refCache: Cache<string, IdentifiableReference<WithMetadata<T>>>
|
|
70
|
+
private listCache: Cache<string, List<WithMetadata<T>>>
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
private readonly firestore: Firestore,
|
|
74
|
+
private readonly collectionPath: string,
|
|
75
|
+
private readonly converter: FirestoreDataConverter<T, SerializedT>,
|
|
76
|
+
private readonly opts?: FirebaseDataManagerOptions
|
|
77
|
+
) {
|
|
78
|
+
// @ts-expect-error - Force merge FirestoreDataConverter and InternalFirestoreDataConverter
|
|
79
|
+
this.mergedConverter = new MergeConverters(this.converter, new FirestoreMetadataConverter())
|
|
80
|
+
this.managerOptions = Object.assign(defaultFirebaseDataManagerOptions, this.opts)
|
|
81
|
+
|
|
82
|
+
this.refCache = this.managerOptions.refCache || new MapCache(100)
|
|
83
|
+
this.listCache = this.managerOptions.listCache || new MapCache(100)
|
|
84
|
+
|
|
85
|
+
this.collection = this.firestore
|
|
86
|
+
.collection(this.firestore.app, this.collectionPath)
|
|
87
|
+
.withConverter(this.mergedConverter)
|
|
88
|
+
|
|
89
|
+
this.collectionQuery =
|
|
90
|
+
this.managerOptions.deleteMode === 'soft'
|
|
91
|
+
? queryNotDeleted(this.collection, this.firestore.query, this.firestore.where)
|
|
92
|
+
: this.collection
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public async read(id: string, options?: FirestoreReadOptions): Promise<WithMetadata<T> | undefined> {
|
|
96
|
+
if (!this.managerOptions?.Reference) throw new Error('ReferenceClass not defined')
|
|
97
|
+
|
|
98
|
+
const ref = this.getRef(id)
|
|
99
|
+
|
|
100
|
+
if (options?.transaction) {
|
|
101
|
+
const snapshot = await options.transaction.get<WithMetadata<T>, DocumentData>(
|
|
102
|
+
this.firestore.doc(this.collection, id)
|
|
103
|
+
)
|
|
104
|
+
if (!checkIfReferenceExists(snapshot)) return undefined
|
|
105
|
+
return snapshot.data()
|
|
106
|
+
}
|
|
107
|
+
return await ref.resolve()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async create(data: WithFieldValue<WithoutId<T>>, options?: FirebaseCreateOptions) {
|
|
111
|
+
let id: string | undefined = undefined
|
|
112
|
+
if (options?.id) {
|
|
113
|
+
id = options?.id
|
|
114
|
+
} else if (this.managerOptions?.idResolver) {
|
|
115
|
+
id = this.managerOptions.idResolver()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let docRef: FirestoreTypes.DocumentReference
|
|
119
|
+
let docExists: boolean = false
|
|
120
|
+
|
|
121
|
+
if (id) {
|
|
122
|
+
docRef = this.firestore.doc(this.collection, id)
|
|
123
|
+
docExists = await this.preventOverwriteOnCreate(docRef, options)
|
|
124
|
+
} else {
|
|
125
|
+
docRef = this.firestore.doc(this.collection)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const docDataWithMetadata = {
|
|
129
|
+
...data,
|
|
130
|
+
[FIRESTORE_KEYS.CREATED_AT]: docExists ? undefined : this.firestore.serverTimestamp(),
|
|
131
|
+
[FIRESTORE_KEYS.UPDATED_AT]: this.firestore.serverTimestamp(),
|
|
132
|
+
[FIRESTORE_KEYS.DELETED]: false,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const firebaseOptions = { merge: options?.merge }
|
|
136
|
+
|
|
137
|
+
if (options?.transaction) {
|
|
138
|
+
options?.transaction.set(docRef, docDataWithMetadata, firebaseOptions)
|
|
139
|
+
} else {
|
|
140
|
+
await this.firestore.setDoc(docRef, docDataWithMetadata, firebaseOptions)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return this.getRef(docRef.id)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async _update(
|
|
147
|
+
id: string,
|
|
148
|
+
data: WithoutId<Partial<WithFieldValue<T & Metadata>>>,
|
|
149
|
+
options?: FirestoreWriteOptions
|
|
150
|
+
) {
|
|
151
|
+
const extendedData = {
|
|
152
|
+
...data,
|
|
153
|
+
[FIRESTORE_KEYS.UPDATED_AT]: this.firestore.serverTimestamp(),
|
|
154
|
+
}
|
|
155
|
+
// Firestore update method doesn't call converter like setDoc does, so we need to serialize the data manually.
|
|
156
|
+
const serializedData = this.mergedConverter.toFirestore(extendedData)
|
|
157
|
+
|
|
158
|
+
const ref = this.firestore.doc(this.collection, id)
|
|
159
|
+
|
|
160
|
+
if (options?.transaction) {
|
|
161
|
+
return options.transaction.update<DocumentData, DocumentData>(ref, serializedData)
|
|
162
|
+
}
|
|
163
|
+
return this.firestore.updateDoc(ref, serializedData)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public async update(id: string, data: WithoutId<Partial<WithFieldValue<T>>>, options?: FirestoreWriteOptions) {
|
|
167
|
+
await this._update(id, data, options)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public async upsert(id: string, data: WithFieldValue<WithoutId<T>>, options?: FirestoreWriteOptions) {
|
|
171
|
+
this.create(data, { ...options, id, merge: true })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async delete(id: string, options?: FirestoreWriteOptions) {
|
|
175
|
+
if (this.managerOptions.deleteMode === 'soft') {
|
|
176
|
+
await this._update(
|
|
177
|
+
id,
|
|
178
|
+
{
|
|
179
|
+
...({} as Partial<T>),
|
|
180
|
+
[FIRESTORE_KEYS.DELETED]: true,
|
|
181
|
+
},
|
|
182
|
+
options
|
|
183
|
+
)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
return this.firestore.deleteDoc(this.firestore.doc(this.collection, id))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public getRef(id: string) {
|
|
190
|
+
if (!this.managerOptions?.Reference) throw new Error('ReferenceClass not defined')
|
|
191
|
+
|
|
192
|
+
if (this.refCache.has(id)) {
|
|
193
|
+
return this.refCache.get(id)!
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const newRef = new this.managerOptions.Reference(this.firestore, this.firestore.doc(this.collection, id), {
|
|
197
|
+
readMode: this.managerOptions.readMode,
|
|
198
|
+
})
|
|
199
|
+
this.refCache.set(id, newRef)
|
|
200
|
+
return newRef
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private _getFilteredQuery(params?: QueryParams<SerializedT>) {
|
|
204
|
+
let compoundQuery = this.collectionQuery
|
|
205
|
+
|
|
206
|
+
params?.filters?.forEach(filter => {
|
|
207
|
+
compoundQuery = this.firestore.query(compoundQuery, this.firestore.where(filter[0], filter[1], filter[2]))
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
params?.orderBy?.forEach(orderBy => {
|
|
211
|
+
compoundQuery = this.firestore.query(compoundQuery, this.firestore.orderBy(orderBy[0], orderBy[1]))
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
return compoundQuery
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public getList(params?: QueryParams<SerializedT> & ListPaginationParams) {
|
|
218
|
+
if (!this.managerOptions.List) throw new Error('ListClass not defined')
|
|
219
|
+
|
|
220
|
+
const compoundQuery = this._getFilteredQuery(params)
|
|
221
|
+
|
|
222
|
+
const key = JSON.stringify(params || {})
|
|
223
|
+
if (this.listCache.has(key)) {
|
|
224
|
+
return this.listCache.get(key)!
|
|
225
|
+
}
|
|
226
|
+
const newList = new this.managerOptions.List(this.firestore, compoundQuery, {
|
|
227
|
+
readMode: this.managerOptions.readMode,
|
|
228
|
+
...params,
|
|
229
|
+
})
|
|
230
|
+
this.listCache.set(key, newList)
|
|
231
|
+
return newList
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async preventOverwriteOnCreate(docRef: FirestoreTypes.DocumentReference, createOptions?: CreateOptions) {
|
|
235
|
+
const doc = await this.firestore.getDoc(docRef)
|
|
236
|
+
const docExists = checkIfReferenceExists(doc)
|
|
237
|
+
if (docExists && this.managerOptions.preventOverwriteOnCreate && createOptions?.merge !== true) {
|
|
238
|
+
throw new Error(`Document already exists - ${doc.ref.path}`)
|
|
239
|
+
}
|
|
240
|
+
return docExists
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ListPaginationParams, LiveList, LiveListOptions } from '@data-weave/datamanager'
|
|
2
|
+
import { DocumentData, Firestore, FirestoreReadMode, FirestoreTypes } from './firestoreTypes'
|
|
3
|
+
|
|
4
|
+
export interface FirestoreListOptions<T> extends LiveListOptions<T> {
|
|
5
|
+
readMode?: FirestoreReadMode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class FirestoreList<T extends DocumentData, S extends DocumentData> extends LiveList<T> {
|
|
9
|
+
private unsubscribeFromSnapshot: undefined | (() => void)
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly firestore: Firestore,
|
|
13
|
+
private readonly query: FirestoreTypes.Query<T, S>,
|
|
14
|
+
private readonly options: FirestoreListOptions<T> & ListPaginationParams
|
|
15
|
+
) {
|
|
16
|
+
super(options)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async resolve() {
|
|
20
|
+
if (this.options?.readMode === 'realtime') {
|
|
21
|
+
let initialLoad = true
|
|
22
|
+
return new Promise<T[]>((resolve, reject) => {
|
|
23
|
+
this.unsubscribeFromSnapshot = this.firestore.onSnapshot(
|
|
24
|
+
this.query,
|
|
25
|
+
querySnapshot => {
|
|
26
|
+
try {
|
|
27
|
+
if (initialLoad) {
|
|
28
|
+
initialLoad = false
|
|
29
|
+
this.handleInitialDataChange(querySnapshot.docs)
|
|
30
|
+
} else {
|
|
31
|
+
this.handleSubsequentDataChanges(querySnapshot.docChanges())
|
|
32
|
+
}
|
|
33
|
+
// TODO: When calling ".resolve()" with realtime listener,
|
|
34
|
+
// the snapshot data might be stale from cache.
|
|
35
|
+
resolve(this.values)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
this.onError(error)
|
|
38
|
+
reject(error)
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
error => {
|
|
42
|
+
this.onError(error)
|
|
43
|
+
reject(error)
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
} else {
|
|
48
|
+
const snapshot = await this.firestore.getDocs(this.query)
|
|
49
|
+
this.handleInitialDataChange(snapshot.docs)
|
|
50
|
+
return this.values
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected handleInitialDataChange(values: FirestoreTypes.QueryDocumentSnapshot<T, S>[]) {
|
|
55
|
+
const newValues = values.map(v => v.data())
|
|
56
|
+
this.onUpdateAll(newValues)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected handleSubsequentDataChanges(changes: FirestoreTypes.DocumentChange<T, S>[]) {
|
|
60
|
+
changes.forEach(change => {
|
|
61
|
+
if (change.type === 'added') {
|
|
62
|
+
this.onAddAtIndex(change.newIndex, change.doc.data())
|
|
63
|
+
} else if (change.type === 'modified') {
|
|
64
|
+
this.onUpdateAtIndex(change.newIndex, change.doc.data())
|
|
65
|
+
} else if (change.type === 'removed') {
|
|
66
|
+
this.onRemoveAtIndex(change.oldIndex)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
// TODO: handle onUpdate in the parent class - make sure it's only called once after all changes are processed
|
|
70
|
+
this.onUpdate()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public unSubscribe() {
|
|
74
|
+
this.unsubscribeFromSnapshot?.()
|
|
75
|
+
this.setStale()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected onError(error: unknown) {
|
|
79
|
+
// Try to provide useful collection details using internal properties
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
console.error(`FirestoreList Collection: ${(this.query as any)?._collectionPath?.id} error`, error)
|
|
82
|
+
super.onError(error)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Metadata, WithoutId } from '@data-weave/datamanager'
|
|
2
|
+
import {
|
|
3
|
+
DocumentData,
|
|
4
|
+
FirestoreQuery,
|
|
5
|
+
FirestoreTypes,
|
|
6
|
+
FirestoreWhere,
|
|
7
|
+
InternalFirestoreDataConverter,
|
|
8
|
+
WithFieldValue,
|
|
9
|
+
} from './firestoreTypes'
|
|
10
|
+
|
|
11
|
+
enum FIRESTORE_INTERAL_KEYS {
|
|
12
|
+
DELETED = '__deleted',
|
|
13
|
+
CREATED_AT = '__createdAt',
|
|
14
|
+
UPDATED_AT = '__updatedAt',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export enum FIRESTORE_KEYS {
|
|
18
|
+
DELETED = 'deleted',
|
|
19
|
+
CREATED_AT = 'createdAt',
|
|
20
|
+
UPDATED_AT = 'updatedAt',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FirestoreSerializedMetadata = {
|
|
24
|
+
[FIRESTORE_INTERAL_KEYS.DELETED]: boolean
|
|
25
|
+
[FIRESTORE_INTERAL_KEYS.CREATED_AT]: FirestoreTypes.Timestamp
|
|
26
|
+
[FIRESTORE_INTERAL_KEYS.UPDATED_AT]: FirestoreTypes.Timestamp
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class FirestoreMetadataConverter
|
|
30
|
+
implements InternalFirestoreDataConverter<Metadata, FirestoreSerializedMetadata>
|
|
31
|
+
{
|
|
32
|
+
toFirestore(data: WithFieldValue<WithoutId<Metadata>>) {
|
|
33
|
+
return {
|
|
34
|
+
[FIRESTORE_INTERAL_KEYS.DELETED]: data[FIRESTORE_KEYS.DELETED],
|
|
35
|
+
// Cast: Firestore handles conversion from Date to Timestamp internally
|
|
36
|
+
[FIRESTORE_INTERAL_KEYS.CREATED_AT]: data[FIRESTORE_KEYS.CREATED_AT] as unknown as FirestoreTypes.Timestamp,
|
|
37
|
+
[FIRESTORE_INTERAL_KEYS.UPDATED_AT]: data[FIRESTORE_KEYS.UPDATED_AT] as unknown as FirestoreTypes.Timestamp,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
fromFirestore(
|
|
41
|
+
snapshot: FirestoreTypes.QueryDocumentSnapshot<DocumentData, DocumentData>,
|
|
42
|
+
options: FirestoreTypes.SnapshotOptions
|
|
43
|
+
) {
|
|
44
|
+
const data = snapshot.data(options) as FirestoreSerializedMetadata
|
|
45
|
+
return {
|
|
46
|
+
id: snapshot.ref.id,
|
|
47
|
+
// TODO: add warning in case data type is not as expected
|
|
48
|
+
createdAt: data[FIRESTORE_INTERAL_KEYS.CREATED_AT]?.toDate(),
|
|
49
|
+
updatedAt: data[FIRESTORE_INTERAL_KEYS.UPDATED_AT]?.toDate(),
|
|
50
|
+
deleted: data[FIRESTORE_INTERAL_KEYS.DELETED],
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const queryNotDeleted = <T extends DocumentData, SerializedT extends DocumentData>(
|
|
56
|
+
query: FirestoreTypes.Query<T, SerializedT>,
|
|
57
|
+
firestoreQuery: FirestoreQuery<T, SerializedT>,
|
|
58
|
+
firestoreWhere: FirestoreWhere
|
|
59
|
+
) => firestoreQuery(query, firestoreWhere(FIRESTORE_INTERAL_KEYS.DELETED, '==', false))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { LiveReference, LiveReferenceOptions } from '@data-weave/datamanager'
|
|
2
|
+
import { DocumentData, Firestore, FirestoreReadMode, FirestoreTypes } from './firestoreTypes'
|
|
3
|
+
import { checkIfReferenceExists } from './utils'
|
|
4
|
+
|
|
5
|
+
export interface FirestoreReferenceOptions<T> extends LiveReferenceOptions<T> {
|
|
6
|
+
readMode?: FirestoreReadMode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class FirestoreReference<T extends DocumentData, S extends DocumentData> extends LiveReference<T> {
|
|
10
|
+
private unsubscribeFromSnapshot: undefined | (() => void)
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly firestore: Firestore,
|
|
14
|
+
private readonly docRef: FirestoreTypes.DocumentReference<T, S>,
|
|
15
|
+
readonly options: FirestoreReferenceOptions<T>
|
|
16
|
+
) {
|
|
17
|
+
super(options)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async resolve(): Promise<T | undefined> {
|
|
21
|
+
if (this.options?.readMode === 'realtime') {
|
|
22
|
+
return new Promise<T | undefined>((res, reject) => {
|
|
23
|
+
this.unsubscribeFromSnapshot = this.firestore.onSnapshot(
|
|
24
|
+
this.docRef,
|
|
25
|
+
documentSnapshot => {
|
|
26
|
+
try {
|
|
27
|
+
this.onUpdate(this.parseDocumentSnapshot(documentSnapshot))
|
|
28
|
+
// TODO: When calling ".resolve()" with realtime listener,
|
|
29
|
+
// the snapshot data might be stale from cache.
|
|
30
|
+
res(this.value)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
this.onError(error)
|
|
33
|
+
reject(error)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
error => {
|
|
37
|
+
this.onError(error)
|
|
38
|
+
reject(error)
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const doc = await this.firestore.getDoc(this.docRef)
|
|
45
|
+
this.onUpdate(this.parseDocumentSnapshot(doc))
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.onError(error)
|
|
48
|
+
throw error
|
|
49
|
+
}
|
|
50
|
+
return this.value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public get id(): string {
|
|
54
|
+
return this.docRef.id
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public get path(): string {
|
|
58
|
+
return this.docRef.path
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected onError(error: unknown): void {
|
|
62
|
+
console.error(`FirestoreReference error ${this.docRef.path}`, error)
|
|
63
|
+
super.onError(error)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public unSubscribe() {
|
|
67
|
+
this.unsubscribeFromSnapshot?.()
|
|
68
|
+
this.setStale()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private parseDocumentSnapshot(docSnapshot: FirestoreTypes.DocumentSnapshot<T, S>): T | undefined {
|
|
72
|
+
if (!checkIfReferenceExists(docSnapshot)) throw new Error(`Document does not exist ${this.docRef.path}`)
|
|
73
|
+
return docSnapshot.data()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { WithoutId } from '@data-weave/datamanager'
|
|
2
|
+
import type FirestoreTypes from '@firebase/firestore'
|
|
3
|
+
import type { FieldPath, WithFieldValue as FirestoreWithFieldValue, QueryConstraint } from '@firebase/firestore'
|
|
4
|
+
import type { HttpsCallable, HttpsCallableOptions } from '@firebase/functions-types'
|
|
5
|
+
import { FieldValue, Transaction } from '@google-cloud/firestore'
|
|
6
|
+
import { injectable } from 'inversify'
|
|
7
|
+
|
|
8
|
+
export type DocumentData = FirestoreTypes.DocumentData
|
|
9
|
+
|
|
10
|
+
export { FieldValue, FirestoreTypes, Transaction }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Same as Partial but all fields are required to be included
|
|
14
|
+
*/
|
|
15
|
+
type OptionallyUndefined<T> = { [P in keyof T]: T[P] | undefined }
|
|
16
|
+
|
|
17
|
+
export declare interface InternalFirestoreDataConverter<
|
|
18
|
+
T extends DocumentData,
|
|
19
|
+
SerializedT extends DocumentData = DocumentData,
|
|
20
|
+
> {
|
|
21
|
+
toFirestore(
|
|
22
|
+
modelObject: Partial<WithFieldValue<WithoutId<T>>>,
|
|
23
|
+
options?: FirestoreTypes.SetOptions
|
|
24
|
+
): WithFieldValue<WithoutId<SerializedT>>
|
|
25
|
+
fromFirestore(
|
|
26
|
+
snapshot: FirestoreTypes.QueryDocumentSnapshot<T, SerializedT>,
|
|
27
|
+
options?: FirestoreTypes.SnapshotOptions
|
|
28
|
+
): T
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converts data between model and serialized data. Differs from InternalFirestoreDataConverter
|
|
33
|
+
* by using `OptionallyUndefined` and `Required` to ensure that all fields are included in de/serialization
|
|
34
|
+
* even if some model defined properties are optional.
|
|
35
|
+
*/
|
|
36
|
+
export declare interface FirestoreDataConverter<
|
|
37
|
+
T extends DocumentData,
|
|
38
|
+
SerializedT extends DocumentData = DocumentData,
|
|
39
|
+
> {
|
|
40
|
+
toFirestore(
|
|
41
|
+
modelObject: WithoutId<Partial<WithFieldValue<T>>>,
|
|
42
|
+
options?: FirestoreTypes.SetOptions
|
|
43
|
+
): OptionallyUndefined<Required<WithoutId<WithFieldValue<SerializedT>>>>
|
|
44
|
+
fromFirestore(
|
|
45
|
+
snapshot: FirestoreTypes.QueryDocumentSnapshot<T, SerializedT>,
|
|
46
|
+
options?: FirestoreTypes.SnapshotOptions
|
|
47
|
+
): OptionallyUndefined<Required<WithoutId<T>>>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type FirestoreQuery<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData> = (
|
|
51
|
+
reference: FirestoreTypes.Query<AppModelType, DbModelType>,
|
|
52
|
+
filter: any
|
|
53
|
+
) => FirestoreTypes.Query<AppModelType, DbModelType>
|
|
54
|
+
export type FirestoreWhere = (field: string | FieldPath, op: string, value: unknown) => any
|
|
55
|
+
|
|
56
|
+
export type FirestoreReadMode = 'realtime' | 'static'
|
|
57
|
+
|
|
58
|
+
export interface FirestoreReadOptions {
|
|
59
|
+
readonly transaction?: FirestoreTypes.Transaction
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FirestoreWriteOptions {
|
|
63
|
+
readonly transaction?: FirestoreTypes.Transaction
|
|
64
|
+
readonly batcher?: FirestoreTypes.WriteBatch
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface FirebaseCreateOptions extends FirestoreWriteOptions {
|
|
68
|
+
id?: string
|
|
69
|
+
merge?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export declare type WithFieldValue<T> = {
|
|
73
|
+
[K in keyof T]: FirestoreWithFieldValue<T[K]> | FirestoreTypes.FieldValue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type GenNode<K extends string, IsRoot extends boolean> = IsRoot extends true ? `${K}` : `.${K}`
|
|
77
|
+
|
|
78
|
+
export type FilterableFields<
|
|
79
|
+
T extends object,
|
|
80
|
+
IsRoot extends boolean = true,
|
|
81
|
+
K extends keyof T = keyof T,
|
|
82
|
+
> = K extends string
|
|
83
|
+
? // Handle firebase timestamp fields
|
|
84
|
+
T[K] extends FirestoreTypes.Timestamp
|
|
85
|
+
? `${K}`
|
|
86
|
+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
T[K] extends any[]
|
|
88
|
+
? GenNode<K, IsRoot>
|
|
89
|
+
: T[K] extends object
|
|
90
|
+
? `${GenNode<K, IsRoot>}${FilterableFields<T[K], false>}`
|
|
91
|
+
: GenNode<K, IsRoot>
|
|
92
|
+
: never
|
|
93
|
+
|
|
94
|
+
type GetValue<T, K extends string> = K extends `${infer L}.${infer R}`
|
|
95
|
+
? L extends keyof T
|
|
96
|
+
? GetValue<T[L], R>
|
|
97
|
+
: never
|
|
98
|
+
: K extends keyof T
|
|
99
|
+
? T[K]
|
|
100
|
+
: K extends string
|
|
101
|
+
? string | number | boolean
|
|
102
|
+
: never
|
|
103
|
+
|
|
104
|
+
export type PrimitiveFilterOp = '<' | '<=' | '==' | '!=' | '>=' | '>'
|
|
105
|
+
|
|
106
|
+
export type FilterBy<T extends object, Field extends string = FilterableFields<T>> = Field extends string
|
|
107
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
GetValue<T, Field> extends any[]
|
|
109
|
+
? [Field, 'array-contains', string] | [Field, 'array-contains-any', string[]]
|
|
110
|
+
: [Field, PrimitiveFilterOp, GetValue<T, Field>] | [Field, 'in', string[]]
|
|
111
|
+
: never
|
|
112
|
+
|
|
113
|
+
export type OrderBy<T extends DocumentData, Fields extends string = FilterableFields<T>> = [
|
|
114
|
+
Fields,
|
|
115
|
+
FirestoreTypes.OrderByDirection,
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
export abstract class FieldValues {
|
|
119
|
+
public abstract serverTimestamp(): FirestoreTypes.FieldValue
|
|
120
|
+
public abstract delete(): FirestoreTypes.FieldValue
|
|
121
|
+
public abstract increment(n: number): FirestoreTypes.FieldValue
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
public abstract arrayUnion(...elements: any[]): FirestoreTypes.FieldValue
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
public abstract arrayRemove(...elements: any[]): FirestoreTypes.FieldValue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**********
|
|
129
|
+
*
|
|
130
|
+
*
|
|
131
|
+
* INJECTABLES
|
|
132
|
+
*
|
|
133
|
+
*
|
|
134
|
+
* **********/
|
|
135
|
+
|
|
136
|
+
export interface FirestoreAppSettings {
|
|
137
|
+
persistence?: boolean
|
|
138
|
+
cacheSizeBytes?: number
|
|
139
|
+
host?: string
|
|
140
|
+
ssl?: boolean
|
|
141
|
+
ignoreUndefinedProperties?: boolean
|
|
142
|
+
serverTimestampBehavior?: 'estimate' | 'previous' | 'none'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export abstract class FirestoreApp {
|
|
146
|
+
public abstract type?: string
|
|
147
|
+
public abstract toJSON?(): object
|
|
148
|
+
public abstract terminate?(): Promise<void>
|
|
149
|
+
public abstract settings?(settings: FirestoreAppSettings): void
|
|
150
|
+
public abstract useEmulator?(host: string, port: number): void
|
|
151
|
+
public abstract runTransaction?<T>(
|
|
152
|
+
updateFunction: (transaction: Transaction) => Promise<T>,
|
|
153
|
+
options?: FirestoreTypes.TransactionOptions
|
|
154
|
+
): Promise<T>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@injectable()
|
|
158
|
+
export abstract class Firestore {
|
|
159
|
+
public abstract app: FirestoreApp
|
|
160
|
+
public abstract collection(reference: FirestoreTypes.CollectionReference | FirestoreApp, path: string): any
|
|
161
|
+
public abstract getDocs<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
162
|
+
reference: FirestoreTypes.Query<AppModelType, DbModelType>
|
|
163
|
+
): Promise<FirestoreTypes.QuerySnapshot<AppModelType, DbModelType>>
|
|
164
|
+
public abstract getDoc<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
165
|
+
reference: FirestoreTypes.DocumentReference<AppModelType, DbModelType>,
|
|
166
|
+
path?: string
|
|
167
|
+
): Promise<FirestoreTypes.DocumentSnapshot<AppModelType, DbModelType>>
|
|
168
|
+
public abstract serverTimestamp(): FirestoreTypes.FieldValue
|
|
169
|
+
public abstract increment(n: number): FirestoreTypes.FieldValue
|
|
170
|
+
public abstract query<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
171
|
+
reference: FirestoreTypes.Query<AppModelType, DbModelType>,
|
|
172
|
+
filter: any
|
|
173
|
+
): FirestoreTypes.Query<AppModelType, DbModelType>
|
|
174
|
+
public abstract where(field: string | FieldPath, op: string, value: unknown): QueryConstraint
|
|
175
|
+
public abstract limit(limit: number): any
|
|
176
|
+
public abstract orderBy(orderBy: string, direction: FirestoreTypes.OrderByDirection): any
|
|
177
|
+
public abstract setDoc<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
178
|
+
reference: FirestoreTypes.DocumentReference<AppModelType, DbModelType>,
|
|
179
|
+
data: any,
|
|
180
|
+
options?: FirestoreTypes.SetOptions
|
|
181
|
+
): Promise<void>
|
|
182
|
+
public abstract updateDoc<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
183
|
+
reference: FirestoreTypes.DocumentReference<AppModelType, DbModelType>,
|
|
184
|
+
data: any
|
|
185
|
+
): Promise<void>
|
|
186
|
+
public abstract doc<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
187
|
+
reference: FirestoreTypes.Query<AppModelType, DbModelType>,
|
|
188
|
+
path?: string
|
|
189
|
+
): FirestoreTypes.DocumentReference<AppModelType, DbModelType>
|
|
190
|
+
public abstract deleteDoc<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
191
|
+
reference: FirestoreTypes.DocumentReference<AppModelType, DbModelType>
|
|
192
|
+
): Promise<void>
|
|
193
|
+
public abstract onSnapshot<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
194
|
+
reference: FirestoreTypes.DocumentReference<AppModelType, DbModelType>,
|
|
195
|
+
onNext: (snapshot: FirestoreTypes.DocumentSnapshot<AppModelType, DbModelType>) => void,
|
|
196
|
+
onError?: (error: Error) => void
|
|
197
|
+
): () => void
|
|
198
|
+
public abstract onSnapshot<AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData>(
|
|
199
|
+
reference: FirestoreTypes.Query<AppModelType, DbModelType>,
|
|
200
|
+
onNext: (snapshot: FirestoreTypes.QuerySnapshot<AppModelType, DbModelType>) => void,
|
|
201
|
+
onError?: (error: Error) => void
|
|
202
|
+
): () => void
|
|
203
|
+
public abstract runTransaction<T>(
|
|
204
|
+
firestore: FirestoreApp,
|
|
205
|
+
transaction: (transaction: Transaction) => Promise<T>,
|
|
206
|
+
options?: FirestoreTypes.TransactionOptions
|
|
207
|
+
): Promise<T>
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@injectable()
|
|
211
|
+
export abstract class FirestoreSettings {
|
|
212
|
+
public abstract readMode: FirestoreReadMode
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@injectable()
|
|
216
|
+
export abstract class FirestoreFunctions {
|
|
217
|
+
public abstract httpsCallable(name: string, options?: HttpsCallableOptions): HttpsCallable
|
|
218
|
+
public abstract useEmulator(host: string, port: number): void
|
|
219
|
+
public abstract useFunctionsEmulator(origin: string): void
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@injectable()
|
|
223
|
+
export class DummyFirestoreFunctions implements FirestoreFunctions {
|
|
224
|
+
public httpsCallable(): HttpsCallable {
|
|
225
|
+
return () => {
|
|
226
|
+
throw new Error('httpsCallable not implemented - DummyFirestoreFunctions')
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
public useEmulator() {}
|
|
230
|
+
public useFunctionsEmulator() {}
|
|
231
|
+
}
|
package/lib/index.ts
ADDED
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { WithoutId } from '@data-weave/datamanager'
|
|
2
|
+
import {
|
|
3
|
+
DocumentData,
|
|
4
|
+
FieldValues,
|
|
5
|
+
Firestore,
|
|
6
|
+
FirestoreApp,
|
|
7
|
+
FirestoreTypes,
|
|
8
|
+
InternalFirestoreDataConverter,
|
|
9
|
+
Transaction,
|
|
10
|
+
WithFieldValue,
|
|
11
|
+
} from './firestoreTypes'
|
|
12
|
+
|
|
13
|
+
export class MergeConverters<
|
|
14
|
+
T extends DocumentData,
|
|
15
|
+
SerializedT extends DocumentData,
|
|
16
|
+
G extends DocumentData,
|
|
17
|
+
SerializedG extends DocumentData,
|
|
18
|
+
> implements InternalFirestoreDataConverter<T & G, SerializedT & SerializedG>
|
|
19
|
+
{
|
|
20
|
+
constructor(
|
|
21
|
+
private converter1: InternalFirestoreDataConverter<T, SerializedT>,
|
|
22
|
+
private converter2: InternalFirestoreDataConverter<G, SerializedG>
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
toFirestore(
|
|
26
|
+
modelObject: WithoutId<Partial<WithFieldValue<T>>> & WithoutId<Partial<WithFieldValue<G>>>,
|
|
27
|
+
options?: FirestoreTypes.SetOptions
|
|
28
|
+
) {
|
|
29
|
+
if (options) {
|
|
30
|
+
return {
|
|
31
|
+
...this.converter1.toFirestore(modelObject, options),
|
|
32
|
+
...this.converter2.toFirestore(modelObject, options),
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
return {
|
|
36
|
+
...this.converter1.toFirestore(modelObject),
|
|
37
|
+
...this.converter2.toFirestore(modelObject),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fromFirestore(
|
|
43
|
+
snapshot: FirestoreTypes.QueryDocumentSnapshot<T & G, SerializedT & SerializedG>,
|
|
44
|
+
options: FirestoreTypes.SnapshotOptions
|
|
45
|
+
) {
|
|
46
|
+
return {
|
|
47
|
+
...this.converter1.fromFirestore(snapshot, options),
|
|
48
|
+
...this.converter2.fromFirestore(snapshot, options),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* FirestoreNamespacedConverter creates an interface between modular and namespaced
|
|
55
|
+
* firestore libraries.
|
|
56
|
+
* - admin sdk doesn't support modular imports
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
// TODO: add types
|
|
60
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
61
|
+
export class FirestoreNamespacedConverter extends Firestore {
|
|
62
|
+
public app: FirestoreApp
|
|
63
|
+
constructor(
|
|
64
|
+
readonly firestore: FirestoreApp,
|
|
65
|
+
readonly fieldValues: FieldValues
|
|
66
|
+
) {
|
|
67
|
+
super()
|
|
68
|
+
this.app = firestore
|
|
69
|
+
this.fieldValues = fieldValues
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public collection(reference: any, path: string) {
|
|
73
|
+
return reference.collection(path)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public getDocs(reference: any) {
|
|
77
|
+
return reference.get()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public getDoc(reference: any, path?: string) {
|
|
81
|
+
return reference.get(path)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public serverTimestamp() {
|
|
85
|
+
return this.fieldValues.serverTimestamp()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public increment(n: number) {
|
|
89
|
+
return this.fieldValues.increment(n)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public query(reference: any, filter: any) {
|
|
93
|
+
if (filter.type === 'where') {
|
|
94
|
+
return reference.where(filter.field, filter.op, filter.value)
|
|
95
|
+
}
|
|
96
|
+
if (filter.type === 'orderBy') {
|
|
97
|
+
return reference.orderBy(filter.field, filter.direction)
|
|
98
|
+
}
|
|
99
|
+
if (filter.type === 'limit') {
|
|
100
|
+
return reference.limit(filter.limit)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public where(field: string | FirestoreTypes.FieldPath, op: string, value: unknown): any {
|
|
105
|
+
return {
|
|
106
|
+
type: 'where',
|
|
107
|
+
field,
|
|
108
|
+
op,
|
|
109
|
+
value,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public limit(limit: number) {
|
|
114
|
+
return {
|
|
115
|
+
type: 'limit',
|
|
116
|
+
limit,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public orderBy(orderBy: string, direction: FirestoreTypes.OrderByDirection) {
|
|
121
|
+
return {
|
|
122
|
+
type: 'orderBy',
|
|
123
|
+
orderBy,
|
|
124
|
+
direction,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public setDoc(reference: any, data: any, options?: FirestoreTypes.SetOptions) {
|
|
129
|
+
return reference.set(data, options)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public updateDoc(reference: any, data: any) {
|
|
133
|
+
return reference.update(data)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public deleteDoc(reference: any) {
|
|
137
|
+
return reference.delete()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public doc(reference: any, path?: string) {
|
|
141
|
+
if (!path) return reference.doc()
|
|
142
|
+
return reference.doc(path)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public onSnapshot(reference: any, onNext: (snapshot: any) => void, onError?: (error: Error) => void) {
|
|
146
|
+
return reference.onSnapshot(onNext, onError)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public runTransaction<T>(
|
|
150
|
+
firestore: FirestoreApp,
|
|
151
|
+
transaction: (transaction: Transaction) => Promise<T>,
|
|
152
|
+
options?: FirestoreTypes.TransactionOptions
|
|
153
|
+
) {
|
|
154
|
+
return firestore.runTransaction!(transaction, options)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
158
|
+
|
|
159
|
+
// Value can be anything
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
|
+
export const isFieldValue = (value: any): value is FirestoreTypes.FieldValue => {
|
|
162
|
+
return value !== undefined && typeof value._toFieldTransform === 'function'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Modular firestore uses exists, namespaced uses exists()
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
167
|
+
export const checkIfReferenceExists = (value: any): boolean => {
|
|
168
|
+
if (value == null) return false
|
|
169
|
+
if (typeof value.exists === 'function') return value.exists()
|
|
170
|
+
return value.exists
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const withTransaction = (
|
|
174
|
+
firestore: Firestore,
|
|
175
|
+
transaction: (transaction: Transaction) => Promise<void>,
|
|
176
|
+
options?: FirestoreTypes.TransactionOptions
|
|
177
|
+
) => {
|
|
178
|
+
return firestore.runTransaction!(firestore.app, transaction, options)
|
|
179
|
+
}
|
package/lib/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@data-weave/backend-firestore",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"author": "",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "genversion --semi lib/version.js",
|
|
9
|
+
"build:clean": "rimraf android/build && rimraf ios/build",
|
|
10
|
+
"prepare": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/data-weave/datamanager/tree/master/packages/backend-firestore"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"firebase": "^11.9.0",
|
|
22
|
+
"firebase-admin": "^13.0.2"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@data-weave/datamanager": "0.3.2"
|
|
26
|
+
},
|
|
27
|
+
"gitHead": "4fc18c650dcea3f3ede1cf2b413399656f90b79a"
|
|
28
|
+
}
|