@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 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
@@ -0,0 +1,6 @@
1
+ export * from './FirestoreDataManager'
2
+ export * from './FirestoreList'
3
+ export * from './FirestoreMetadata'
4
+ export * from './FirestoreReference'
5
+ export * from './firestoreTypes'
6
+ export * from './utils'
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
@@ -0,0 +1,2 @@
1
+ // Generated by genversion.
2
+ module.exports = '0.3.3';
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
+ }