@data-weave/backend-firestore 0.4.2 → 0.4.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.
@@ -1,247 +0,0 @@
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, FirestoreListContext } from './FirestoreList'
8
- import {
9
- FIRESTORE_KEYS,
10
- FirestoreMetadataConverter,
11
- FirestoreSerializedMetadata,
12
- queryNotDeleted,
13
- } from './FirestoreMetadata'
14
- import { FirestoreReference, FirestoreReferenceContext } 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
- readonly errorInterceptor?: (error: unknown, ctx: FirestoreReferenceContext | FirestoreListContext) => void
39
- readonly snapshotOptions?: FirestoreTypes.SnapshotOptions
40
- // TODO: Add preventUpdateIfNotExists?
41
- readonly Reference?: typeof FirestoreReference
42
- readonly List?: typeof FirestoreList
43
- readonly listCache?: Cache
44
- readonly refCache?: Cache
45
- }
46
-
47
- const defaultFirebaseDataManagerOptions: FirebaseDataManagerOptions = {
48
- deleteMode: 'soft',
49
- preventOverwriteOnCreate: true,
50
- readMode: 'static',
51
- Reference: FirestoreReference,
52
- List: FirestoreList,
53
- }
54
-
55
- export interface QueryParams<T extends FirestoreTypes.DocumentData> {
56
- readonly filters?: Array<FilterBy<T & FirestoreSerializedMetadata>>
57
- readonly orderBy?: Array<OrderBy<T & FirestoreSerializedMetadata>>
58
- }
59
-
60
- @injectable()
61
- export class FirestoreDataManager<
62
- T extends FirestoreTypes.DocumentData,
63
- SerializedT extends FirestoreTypes.DocumentData = T,
64
- > implements DataManager<T>
65
- {
66
- private mergedConverter: InternalFirestoreDataConverter<T & Metadata, SerializedT & FirestoreSerializedMetadata>
67
- private collection: FirestoreTypes.CollectionReference<T & Metadata, SerializedT & FirestoreSerializedMetadata>
68
- private collectionQuery: FirestoreTypes.Query<T & Metadata, SerializedT & FirestoreSerializedMetadata>
69
- private managerOptions: FirebaseDataManagerOptions
70
-
71
- private refCache: Cache<string, IdentifiableReference<WithMetadata<T>>>
72
- private listCache: Cache<string, List<WithMetadata<T>>>
73
-
74
- constructor(
75
- private readonly firestore: Firestore,
76
- private readonly collectionPath: string,
77
- private readonly converter: FirestoreDataConverter<T, SerializedT>,
78
- private readonly opts?: FirebaseDataManagerOptions
79
- ) {
80
- // @ts-expect-error - Force merge FirestoreDataConverter and InternalFirestoreDataConverter
81
- this.mergedConverter = new MergeConverters(this.converter, new FirestoreMetadataConverter())
82
- this.managerOptions = Object.assign(defaultFirebaseDataManagerOptions, this.opts)
83
-
84
- this.refCache = this.managerOptions.refCache || new MapCache(100)
85
- this.listCache = this.managerOptions.listCache || new MapCache(100)
86
-
87
- this.collection = this.firestore
88
- .collection(this.firestore.app, this.collectionPath)
89
- .withConverter(this.mergedConverter)
90
-
91
- this.collectionQuery =
92
- this.managerOptions.deleteMode === 'soft'
93
- ? queryNotDeleted(this.collection, this.firestore.query, this.firestore.where)
94
- : this.collection
95
- }
96
-
97
- public async read(id: string, options?: FirestoreReadOptions): Promise<WithMetadata<T> | undefined> {
98
- if (!this.managerOptions?.Reference) throw new Error('ReferenceClass not defined')
99
-
100
- const ref = this.getRef(id)
101
-
102
- if (options?.transaction) {
103
- const snapshot = await options.transaction.get<WithMetadata<T>, DocumentData>(
104
- this.firestore.doc(this.collection, id)
105
- )
106
- if (!checkIfReferenceExists(snapshot)) return undefined
107
- return snapshot.data()
108
- }
109
- return await ref.resolve()
110
- }
111
-
112
- public async create(data: WithFieldValue<WithoutId<T>>, options?: FirebaseCreateOptions) {
113
- let id: string | undefined = undefined
114
- if (options?.id) {
115
- id = options?.id
116
- } else if (this.managerOptions?.idResolver) {
117
- id = this.managerOptions.idResolver()
118
- }
119
-
120
- let docRef: FirestoreTypes.DocumentReference
121
- let docExists: boolean = false
122
-
123
- if (id) {
124
- docRef = this.firestore.doc(this.collection, id)
125
- docExists = await this.preventOverwriteOnCreate(docRef, options)
126
- } else {
127
- docRef = this.firestore.doc(this.collection)
128
- }
129
-
130
- const docDataWithMetadata = {
131
- ...data,
132
- [FIRESTORE_KEYS.CREATED_AT]: docExists ? undefined : this.firestore.serverTimestamp(),
133
- [FIRESTORE_KEYS.UPDATED_AT]: this.firestore.serverTimestamp(),
134
- [FIRESTORE_KEYS.DELETED]: false,
135
- }
136
-
137
- const firebaseOptions = { merge: options?.merge }
138
-
139
- if (options?.transaction) {
140
- options?.transaction.set(docRef, docDataWithMetadata, firebaseOptions)
141
- } else {
142
- await this.firestore.setDoc(docRef, docDataWithMetadata, firebaseOptions)
143
- }
144
-
145
- return this.getRef(docRef.id)
146
- }
147
-
148
- private async _update(
149
- id: string,
150
- data: WithoutId<Partial<WithFieldValue<T & Metadata>>>,
151
- options?: FirestoreWriteOptions
152
- ) {
153
- const extendedData = {
154
- ...data,
155
- [FIRESTORE_KEYS.UPDATED_AT]: this.firestore.serverTimestamp(),
156
- }
157
- // Firestore update method doesn't call converter like setDoc does, so we need to serialize the data manually.
158
- const serializedData = this.mergedConverter.toFirestore(extendedData)
159
-
160
- const ref = this.firestore.doc(this.collection, id)
161
-
162
- if (options?.transaction) {
163
- return options.transaction.update<DocumentData, DocumentData>(ref, serializedData)
164
- }
165
- return this.firestore.updateDoc(ref, serializedData)
166
- }
167
-
168
- public async update(id: string, data: WithoutId<Partial<WithFieldValue<T>>>, options?: FirestoreWriteOptions) {
169
- await this._update(id, data, options)
170
- }
171
-
172
- public async upsert(id: string, data: WithFieldValue<WithoutId<T>>, options?: FirestoreWriteOptions) {
173
- this.create(data, { ...options, id, merge: true })
174
- }
175
-
176
- public async delete(id: string, options?: FirestoreWriteOptions) {
177
- if (this.managerOptions.deleteMode === 'soft') {
178
- await this._update(
179
- id,
180
- {
181
- ...({} as Partial<T>),
182
- [FIRESTORE_KEYS.DELETED]: true,
183
- },
184
- options
185
- )
186
- return
187
- }
188
- return this.firestore.deleteDoc(this.firestore.doc(this.collection, id))
189
- }
190
-
191
- public getRef(id: string) {
192
- if (!this.managerOptions?.Reference) throw new Error('ReferenceClass not defined')
193
-
194
- if (this.refCache.has(id)) {
195
- return this.refCache.get(id)!
196
- }
197
-
198
- const newRef = new this.managerOptions.Reference(this.firestore, this.firestore.doc(this.collection, id), {
199
- readMode: this.managerOptions.readMode,
200
- errorInterceptor: this.managerOptions.errorInterceptor,
201
- snapshotOptions: this.managerOptions.snapshotOptions,
202
- })
203
- this.refCache.set(id, newRef)
204
- return newRef
205
- }
206
-
207
- private _getFilteredQuery(params?: QueryParams<SerializedT>) {
208
- let compoundQuery = this.collectionQuery
209
-
210
- params?.filters?.forEach(filter => {
211
- compoundQuery = this.firestore.query(compoundQuery, this.firestore.where(filter[0], filter[1], filter[2]))
212
- })
213
-
214
- params?.orderBy?.forEach(orderBy => {
215
- compoundQuery = this.firestore.query(compoundQuery, this.firestore.orderBy(orderBy[0], orderBy[1]))
216
- })
217
-
218
- return compoundQuery
219
- }
220
-
221
- public getList(params?: QueryParams<SerializedT> & ListPaginationParams) {
222
- if (!this.managerOptions.List) throw new Error('ListClass not defined')
223
-
224
- const compoundQuery = this._getFilteredQuery(params)
225
-
226
- const key = JSON.stringify(params || {})
227
- if (this.listCache.has(key)) {
228
- return this.listCache.get(key)!
229
- }
230
- const newList = new this.managerOptions.List(this.firestore, compoundQuery, {
231
- readMode: this.managerOptions.readMode,
232
- errorInterceptor: this.managerOptions.errorInterceptor,
233
- ...params,
234
- })
235
- this.listCache.set(key, newList)
236
- return newList
237
- }
238
-
239
- private async preventOverwriteOnCreate(docRef: FirestoreTypes.DocumentReference, createOptions?: CreateOptions) {
240
- const doc = await this.firestore.getDoc(docRef)
241
- const docExists = checkIfReferenceExists(doc)
242
- if (docExists && this.managerOptions.preventOverwriteOnCreate && createOptions?.merge !== true) {
243
- throw new Error(`Document already exists - ${doc.ref.path}`)
244
- }
245
- return docExists
246
- }
247
- }
@@ -1,96 +0,0 @@
1
- import { ListPaginationParams, LiveList, LiveListOptions } from '@data-weave/datamanager'
2
- import { DocumentData, Firestore, FirestoreReadMode, FirestoreTypes } from './firestoreTypes'
3
-
4
- export interface FirestoreListContext {
5
- query: FirestoreTypes.Query<unknown>
6
- readMode?: FirestoreReadMode
7
- type: 'list'
8
- }
9
-
10
- export interface FirestoreListOptions<T> extends LiveListOptions<T> {
11
- readMode?: FirestoreReadMode
12
- errorInterceptor?: (error: unknown, ctx: FirestoreListContext) => void
13
- }
14
-
15
- export class FirestoreList<T extends DocumentData, S extends DocumentData> extends LiveList<T> {
16
- private unsubscribeFromSnapshot: undefined | (() => void)
17
-
18
- constructor(
19
- private readonly firestore: Firestore,
20
- private readonly query: FirestoreTypes.Query<T, S>,
21
- private readonly options: FirestoreListOptions<T> & ListPaginationParams
22
- ) {
23
- super(options)
24
- }
25
-
26
- public async resolve() {
27
- if (this.options?.readMode === 'realtime') {
28
- let initialLoad = true
29
- return new Promise<T[]>((resolve, reject) => {
30
- this.unsubscribeFromSnapshot = this.firestore.onSnapshot(
31
- this.query,
32
- querySnapshot => {
33
- try {
34
- if (initialLoad) {
35
- initialLoad = false
36
- this.handleInitialDataChange(querySnapshot.docs)
37
- } else {
38
- this.handleSubsequentDataChanges(querySnapshot.docChanges())
39
- }
40
- // TODO: When calling ".resolve()" with realtime listener,
41
- // the snapshot data might be stale from cache.
42
- resolve(this.values)
43
- } catch (error) {
44
- this.onError(error)
45
- reject(error)
46
- }
47
- },
48
- error => {
49
- this.onError(error)
50
- reject(error)
51
- }
52
- )
53
- })
54
- } else {
55
- const snapshot = await this.firestore.getDocs(this.query)
56
- this.handleInitialDataChange(snapshot.docs)
57
- return this.values
58
- }
59
- }
60
-
61
- protected handleInitialDataChange(values: FirestoreTypes.QueryDocumentSnapshot<T, S>[]) {
62
- const newValues = values.map(v => v.data())
63
- this.onUpdateAll(newValues)
64
- }
65
-
66
- protected handleSubsequentDataChanges(changes: FirestoreTypes.DocumentChange<T, S>[]) {
67
- changes.forEach(change => {
68
- if (change.type === 'added') {
69
- this.onAddAtIndex(change.newIndex, change.doc.data())
70
- } else if (change.type === 'modified') {
71
- this.onUpdateAtIndex(change.newIndex, change.doc.data())
72
- } else if (change.type === 'removed') {
73
- this.onRemoveAtIndex(change.oldIndex)
74
- }
75
- })
76
- // TODO: handle onUpdate in the parent class - make sure it's only called once after all changes are processed
77
- this.onUpdate()
78
- }
79
-
80
- public unSubscribe() {
81
- this.unsubscribeFromSnapshot?.()
82
- this.setStale()
83
- }
84
-
85
- protected onError(error: unknown) {
86
- // Try to provide useful collection details using internal properties
87
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
- console.error(`FirestoreList Collection: ${(this.query as any)?._collectionPath?.id} error`, error)
89
- this.options?.errorInterceptor?.(error, {
90
- query: this.query,
91
- readMode: this.options?.readMode,
92
- type: 'list',
93
- })
94
- super.onError(error)
95
- }
96
- }
@@ -1,59 +0,0 @@
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))
@@ -1,92 +0,0 @@
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 FirestoreReferenceContext {
6
- path: string
7
- id: string
8
- readMode?: FirestoreReadMode
9
- snapshotOptions?: FirestoreTypes.SnapshotOptions
10
- type: 'reference'
11
- }
12
-
13
- export interface FirestoreReferenceOptions<T> extends LiveReferenceOptions<T> {
14
- readMode?: FirestoreReadMode
15
- snapshotOptions?: FirestoreTypes.SnapshotOptions
16
- errorInterceptor?: (error: unknown, ctx: FirestoreReferenceContext) => void
17
- }
18
-
19
- export class FirestoreReference<T extends DocumentData, S extends DocumentData> extends LiveReference<T> {
20
- private unsubscribeFromSnapshot: undefined | (() => void)
21
-
22
- constructor(
23
- private readonly firestore: Firestore,
24
- private readonly docRef: FirestoreTypes.DocumentReference<T, S>,
25
- readonly options: FirestoreReferenceOptions<T>
26
- ) {
27
- super(options)
28
- }
29
-
30
- public async resolve(): Promise<T | undefined> {
31
- if (this.options?.readMode === 'realtime') {
32
- return new Promise<T | undefined>((res, reject) => {
33
- this.unsubscribeFromSnapshot = this.firestore.onSnapshot(
34
- this.docRef,
35
- documentSnapshot => {
36
- try {
37
- this.onUpdate(this.parseDocumentSnapshot(documentSnapshot))
38
- // TODO: When calling ".resolve()" with realtime listener,
39
- // the snapshot data might be stale from cache.
40
- res(this.value)
41
- } catch (error) {
42
- this.onError(error)
43
- reject(error)
44
- }
45
- },
46
- error => {
47
- this.onError(error)
48
- reject(error)
49
- }
50
- )
51
- })
52
- }
53
- try {
54
- const doc = await this.firestore.getDoc(this.docRef)
55
- this.onUpdate(this.parseDocumentSnapshot(doc))
56
- } catch (error) {
57
- this.onError(error)
58
- throw error
59
- }
60
- return this.value
61
- }
62
-
63
- public get id(): string {
64
- return this.docRef.id
65
- }
66
-
67
- public get path(): string {
68
- return this.docRef.path
69
- }
70
-
71
- protected onError(error: unknown): void {
72
- console.error(`FirestoreReference error ${this.docRef.path}`, error)
73
- this.options?.errorInterceptor?.(error, {
74
- path: this.docRef.path,
75
- id: this.docRef.id,
76
- readMode: this.options?.readMode,
77
- snapshotOptions: this.options?.snapshotOptions,
78
- type: 'reference',
79
- })
80
- super.onError(error)
81
- }
82
-
83
- public unSubscribe() {
84
- this.unsubscribeFromSnapshot?.()
85
- this.setStale()
86
- }
87
-
88
- private parseDocumentSnapshot(docSnapshot: FirestoreTypes.DocumentSnapshot<T, S>): T | undefined {
89
- if (!checkIfReferenceExists(docSnapshot)) throw new Error(`Document does not exist ${this.docRef.path}`)
90
- return docSnapshot.data(this.options?.snapshotOptions)
91
- }
92
- }