@entropic-bond/firebase 1.12.0 → 1.13.0
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/.firebaserc +5 -0
- package/.github/workflows/release.yml +27 -0
- package/CHANGELOG.md +331 -0
- package/firebase.json +30 -0
- package/firestore.indexes.json +4 -0
- package/firestore.rules +8 -0
- package/functions/package-lock.json +2344 -0
- package/functions/package.json +26 -0
- package/functions/src/index.ts +33 -0
- package/functions/tsconfig.json +19 -0
- package/package.json +15 -12
- package/src/auth/firebase-auth.spec.ts +90 -0
- package/src/auth/firebase-auth.ts +212 -0
- package/src/cloud-functions/firebase-cloud-functions.spec.ts +47 -0
- package/src/cloud-functions/firebase-cloud-functions.ts +25 -0
- package/src/cloud-storage/firebase-cloud-storage.spec.ts +135 -0
- package/src/cloud-storage/firebase-cloud-storage.ts +67 -0
- package/src/firebase-helper.ts +92 -0
- package/src/index.ts +5 -0
- package/src/mocks/mock-data.json +148 -0
- package/src/mocks/test-user.ts +121 -0
- package/src/store/firebase-datasource.spec.ts +555 -0
- package/src/store/firebase-datasource.ts +146 -0
- package/storage.rules +8 -0
- package/tsconfig-build.json +7 -0
- package/tsconfig.json +30 -0
- package/vite.config.ts +23 -0
- package/lib/auth/firebase-auth.d.ts +0 -20
- package/lib/auth/firebase-auth.js +0 -189
- package/lib/auth/firebase-auth.js.map +0 -1
- package/lib/cloud-functions/firebase-cloud-functions.d.ts +0 -7
- package/lib/cloud-functions/firebase-cloud-functions.js +0 -27
- package/lib/cloud-functions/firebase-cloud-functions.js.map +0 -1
- package/lib/cloud-storage/firebase-cloud-storage.d.ts +0 -10
- package/lib/cloud-storage/firebase-cloud-storage.js +0 -70
- package/lib/cloud-storage/firebase-cloud-storage.js.map +0 -1
- package/lib/firebase-helper.d.ts +0 -38
- package/lib/firebase-helper.js +0 -57
- package/lib/firebase-helper.js.map +0 -1
- package/lib/index.d.ts +0 -5
- package/lib/index.js +0 -22
- package/lib/index.js.map +0 -1
- package/lib/mocks/test-user.d.ts +0 -49
- package/lib/mocks/test-user.js +0 -134
- package/lib/mocks/test-user.js.map +0 -1
- package/lib/store/firebase-datasource.d.ts +0 -19
- package/lib/store/firebase-datasource.js +0 -117
- package/lib/store/firebase-datasource.js.map +0 -1
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import dns from 'node:dns'
|
|
2
|
+
import { Model, Persistent, Store } from 'entropic-bond'
|
|
3
|
+
import { FirebaseDatasource } from './firebase-datasource'
|
|
4
|
+
import { FirebaseHelper } from '../firebase-helper'
|
|
5
|
+
import { TestUser, DerivedUser, SubClass } from '../mocks/test-user'
|
|
6
|
+
import mockData from '../mocks/mock-data.json'
|
|
7
|
+
|
|
8
|
+
dns.setDefaultResultOrder('ipv4first')
|
|
9
|
+
|
|
10
|
+
async function loadTestData( model: Model<TestUser> ) {
|
|
11
|
+
const users = Object.values( mockData.TestUser )
|
|
12
|
+
await Promise.all(
|
|
13
|
+
users.map( userObj => {
|
|
14
|
+
const user = Persistent.createInstance<TestUser>( userObj as any )
|
|
15
|
+
return model.save( user )
|
|
16
|
+
})
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe( 'Firestore Model', ()=>{
|
|
21
|
+
let model: Model<TestUser>
|
|
22
|
+
let testUser: TestUser
|
|
23
|
+
const host = 'localhost'
|
|
24
|
+
const firestorePort = 9080
|
|
25
|
+
|
|
26
|
+
beforeAll(()=>{
|
|
27
|
+
FirebaseHelper.setFirebaseConfig({
|
|
28
|
+
projectId: "demo-test",
|
|
29
|
+
})
|
|
30
|
+
FirebaseHelper.useEmulator({ host, firestorePort })
|
|
31
|
+
Store.useDataSource( new FirebaseDatasource() )
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
beforeEach( async ()=>{
|
|
35
|
+
|
|
36
|
+
testUser = new TestUser()
|
|
37
|
+
testUser.name = {
|
|
38
|
+
firstName: 'testUserFirstName',
|
|
39
|
+
lastName: 'testUserLastName'
|
|
40
|
+
}
|
|
41
|
+
testUser.age = 35
|
|
42
|
+
testUser.skills = [ 'lazy', 'dirty' ]
|
|
43
|
+
|
|
44
|
+
model = Store.getModel<TestUser>( 'TestUser' )
|
|
45
|
+
|
|
46
|
+
await loadTestData( model )
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
afterEach( async ()=>{
|
|
50
|
+
await fetch( `http://${ host }:${ firestorePort }/emulator/v1/projects/demo-test/databases/(default)/documents`, {
|
|
51
|
+
method: 'DELETE'
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it( 'should find document by id', async ()=>{
|
|
56
|
+
await model.save( testUser )
|
|
57
|
+
|
|
58
|
+
const user = await model.findById( testUser.id )
|
|
59
|
+
|
|
60
|
+
expect( user ).toBeInstanceOf( TestUser )
|
|
61
|
+
expect( user?.id ).toEqual( testUser.id )
|
|
62
|
+
expect( user?.name?.firstName ).toEqual( 'testUserFirstName' )
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it( 'should write a document', async ()=>{
|
|
66
|
+
await model.save( testUser )
|
|
67
|
+
const newUser = await model.findById( testUser.id )
|
|
68
|
+
|
|
69
|
+
expect( newUser?.name ).toEqual({
|
|
70
|
+
firstName: 'testUserFirstName',
|
|
71
|
+
lastName: 'testUserLastName'
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it( 'should delete a document by id', async ()=>{
|
|
76
|
+
await model.save( testUser )
|
|
77
|
+
|
|
78
|
+
const newUser = await model.findById( testUser.id )
|
|
79
|
+
expect( newUser?.age ).toBe( 35 )
|
|
80
|
+
|
|
81
|
+
await model.delete( testUser.id )
|
|
82
|
+
|
|
83
|
+
const deletedUser = await model.findById( testUser.id )
|
|
84
|
+
expect( deletedUser ).toBeUndefined()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it( 'should not throw if a document id doesn\'t exists', ()=>{
|
|
88
|
+
expect( async ()=>{
|
|
89
|
+
await model.findById( 'nonExistingId' )
|
|
90
|
+
}).not.toThrow()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it( 'should return undefined if a document id doesn\'t exists', async ()=>{
|
|
94
|
+
expect( await model.findById( 'nonExistingId' ) ).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it( 'should retrieve array fields', async ()=>{
|
|
98
|
+
await model.save( testUser )
|
|
99
|
+
const newUser = await model.findById( testUser.id )
|
|
100
|
+
|
|
101
|
+
expect( Array.isArray( newUser?.skills ) ).toBeTruthy()
|
|
102
|
+
expect( newUser?.skills ).toEqual( expect.arrayContaining([ 'lazy', 'dirty' ]) )
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it( 'should retrieve object fields', async ()=>{
|
|
106
|
+
await model.save( testUser )
|
|
107
|
+
const newUser = await model.findById( testUser.id )
|
|
108
|
+
|
|
109
|
+
expect( newUser?.name ).toEqual({
|
|
110
|
+
firstName: 'testUserFirstName',
|
|
111
|
+
lastName: 'testUserLastName'
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe( 'Generic find', ()=>{
|
|
116
|
+
it( 'should query all admins with query object', async ()=>{
|
|
117
|
+
testUser.admin = true
|
|
118
|
+
await model.save( testUser )
|
|
119
|
+
|
|
120
|
+
const admins = await model.query({
|
|
121
|
+
operations: [{
|
|
122
|
+
property: 'admin',
|
|
123
|
+
operator: '==',
|
|
124
|
+
value: true
|
|
125
|
+
}]
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect( admins.length ).toBeGreaterThanOrEqual( 1 )
|
|
129
|
+
expect( admins[ 0 ] ).toBeInstanceOf( TestUser )
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it( 'should find all admins with where methods', async ()=>{
|
|
133
|
+
const admins = await model.find().where( 'admin', '==', true ).get()
|
|
134
|
+
|
|
135
|
+
expect( admins.length ).toBeGreaterThanOrEqual( 1 )
|
|
136
|
+
expect( admins[ 0 ] ).toBeInstanceOf( TestUser )
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it( 'should find admins with age less than 56', async ()=>{
|
|
140
|
+
const admins = await model.find()
|
|
141
|
+
.where( 'admin', '==', true )
|
|
142
|
+
.where( 'age', '<', 50 )
|
|
143
|
+
.get()
|
|
144
|
+
|
|
145
|
+
expect( admins.length ).toBeGreaterThanOrEqual( 1 )
|
|
146
|
+
expect( admins[ 0 ]?.age ).toBeLessThan( 50 )
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it( 'should query by subproperties', async ()=>{
|
|
150
|
+
const users = await model.query({
|
|
151
|
+
operations: [
|
|
152
|
+
{
|
|
153
|
+
property: 'name',
|
|
154
|
+
operator: '==',
|
|
155
|
+
value: { firstName: 'userFirstName3' }
|
|
156
|
+
},
|
|
157
|
+
{ property: 'age', operator: '!=', value: 134 }
|
|
158
|
+
]
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
expect( users[0]?.id ).toBe( 'user3' )
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it( 'should find by subproperties', async ()=>{
|
|
165
|
+
const users = await model.find()
|
|
166
|
+
.where( 'name', '==', { firstName: 'userFirstName3' })
|
|
167
|
+
.get()
|
|
168
|
+
|
|
169
|
+
expect( users[0]?.id ).toBe( 'user3' )
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it( 'should find by property path', async ()=>{
|
|
173
|
+
const users = await model.find()
|
|
174
|
+
.whereDeepProp( 'name.firstName', '==', 'userFirstName3' )
|
|
175
|
+
.get()
|
|
176
|
+
|
|
177
|
+
expect( users[0]?.id ).toBe( 'user3' )
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it( 'should find by superdeep property path', async ()=>{
|
|
181
|
+
const users = await model.find()
|
|
182
|
+
.whereDeepProp( 'name.ancestorName.father', '==', 'user3Father')
|
|
183
|
+
.get()
|
|
184
|
+
|
|
185
|
+
expect( users[0]?.id ).toEqual( 'user3' )
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it( 'should find by swallow property path', async ()=>{
|
|
189
|
+
const users = await model.find()
|
|
190
|
+
.whereDeepProp( 'age', '==', 21 )
|
|
191
|
+
.get()
|
|
192
|
+
|
|
193
|
+
expect( users[0]?.id ).toEqual( 'user2' )
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
describe( 'Compound queries', ()=>{
|
|
199
|
+
it( 'should find documents using `AND` compound query', async ()=>{
|
|
200
|
+
const admins = await model.find()
|
|
201
|
+
.where( 'admin', '==', true )
|
|
202
|
+
.where( 'age', '<', 50 )
|
|
203
|
+
.get()
|
|
204
|
+
|
|
205
|
+
expect( admins ).toHaveLength( 1 )
|
|
206
|
+
expect( admins[0]?.age ).toBeLessThan( 50 )
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it( 'should find using `OR` query', async ()=>{
|
|
210
|
+
const docs = await model.find().or( 'age', '==', 23 ).or( 'age', '==', 41 ).get()
|
|
211
|
+
|
|
212
|
+
expect( docs ).toHaveLength( 2 )
|
|
213
|
+
expect( docs ).toEqual( expect.arrayContaining([
|
|
214
|
+
expect.objectContaining({ id: 'user1', age: 23 }),
|
|
215
|
+
expect.objectContaining({ id: 'user5', age: 41 })
|
|
216
|
+
]))
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it( 'should find combining `OR` query and `where` query', async ()=>{
|
|
220
|
+
const docs = await model.find().where( 'age', '>', 50 ).or( 'age', '==', 23 ).or( 'age', '==', 41 ).get()
|
|
221
|
+
|
|
222
|
+
expect( docs ).toHaveLength( 3 )
|
|
223
|
+
expect( docs ).toEqual( expect.arrayContaining([
|
|
224
|
+
expect.objectContaining({ id: 'user1', age: 23 }),
|
|
225
|
+
expect.objectContaining({ id: 'user5', age: 41 }),
|
|
226
|
+
expect.objectContaining({ id: 'user3', age: 56 })
|
|
227
|
+
]))
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it( 'should find combining `OR` query and `where` query in a range', async ()=>{
|
|
231
|
+
const docs = await model.find().where( 'age', '<', 22 ).or( 'age', '>', 50 ).get()
|
|
232
|
+
|
|
233
|
+
expect( docs ).toHaveLength( 2 )
|
|
234
|
+
expect( docs ).toEqual( expect.arrayContaining([
|
|
235
|
+
expect.objectContaining({ id: 'user2', age: 21 }),
|
|
236
|
+
expect.objectContaining({ id: 'user3', age: 56 })
|
|
237
|
+
]))
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it( 'should find combining `OR` query and `where` query on different fields', async ()=>{
|
|
241
|
+
const docs = await model.find().where( 'age', '==', 23 ).or( 'admin', '!=', true ).get()
|
|
242
|
+
|
|
243
|
+
expect( docs ).toHaveLength( 3 )
|
|
244
|
+
expect( docs ).toEqual( expect.arrayContaining([
|
|
245
|
+
expect.objectContaining({ id: 'user1', admin: true, age: 23 }),
|
|
246
|
+
expect.objectContaining({ id: 'user2', admin: false, age: 21 }),
|
|
247
|
+
expect.objectContaining({ id: 'user4', admin: false, age: 35 }),
|
|
248
|
+
]))
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it( 'should throw if a `where` query is used after an `or` query', ()=>{
|
|
252
|
+
expect(
|
|
253
|
+
()=> model.find().or( 'age', '==', 23 ).where( 'age', '>', 50 )
|
|
254
|
+
).toThrow( Model.error.invalidQueryOrder )
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it( 'should evaluate mixing operands', async ()=>{
|
|
258
|
+
const docs = await model.find().where( 'age', '>', 39 ).and( 'age', '<', 57 ).or( 'age', '==', 23 ).or( 'age', '==', 21 ).get()
|
|
259
|
+
expect( docs ).toHaveLength( 5 )
|
|
260
|
+
expect( docs ).toEqual( expect.arrayContaining([
|
|
261
|
+
expect.objectContaining({ id: 'user1', age: 23 }),
|
|
262
|
+
expect.objectContaining({ id: 'user2', age: 21 }),
|
|
263
|
+
expect.objectContaining({ id: 'user3', age: 56 }),
|
|
264
|
+
expect.objectContaining({ id: 'user5', age: 41 }),
|
|
265
|
+
expect.objectContaining({ id: 'user6', age: 40 })
|
|
266
|
+
]))
|
|
267
|
+
|
|
268
|
+
const docs1 = await model.find().where( 'age', '==', 41 ).and( 'age', '==', 56 ).or( 'age', '==', 23 ).or( 'age', '==', 21 ).get()
|
|
269
|
+
expect( docs1 ).toHaveLength( 2 )
|
|
270
|
+
expect( docs1 ).toEqual( expect.arrayContaining([
|
|
271
|
+
expect.objectContaining({ id: 'user1', age: 23 }),
|
|
272
|
+
expect.objectContaining({ id: 'user2', age: 21 })
|
|
273
|
+
]))
|
|
274
|
+
|
|
275
|
+
const docs2 = await model.find().where( 'age', '==', 41 ).or( 'age', '==', 56 ).or( 'age', '==', 23 ).or( 'age', '==', 21 ).get()
|
|
276
|
+
expect( docs2 ).toHaveLength( 4 )
|
|
277
|
+
expect( docs2 ).toEqual( expect.arrayContaining([
|
|
278
|
+
expect.objectContaining({ id: 'user1', age: 23 }),
|
|
279
|
+
expect.objectContaining({ id: 'user2', age: 21 }),
|
|
280
|
+
expect.objectContaining({ id: 'user3', age: 56 }),
|
|
281
|
+
expect.objectContaining({ id: 'user5', age: 41 })
|
|
282
|
+
]))
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe( 'Searchable array property', ()=>{
|
|
288
|
+
|
|
289
|
+
it( 'should find documents using `containsAny` operator', async ()=>{
|
|
290
|
+
const colleague1 = new TestUser( 'colleague1' )
|
|
291
|
+
const colleague2 = new TestUser( 'colleague2' )
|
|
292
|
+
const docs = await model.find().where( 'colleagues', 'containsAny', [ colleague1, colleague2 ]).get()
|
|
293
|
+
|
|
294
|
+
expect( docs ).toHaveLength( 3 )
|
|
295
|
+
expect( docs ).toEqual( expect.arrayContaining([
|
|
296
|
+
expect.objectContaining({ id: 'user2' }),
|
|
297
|
+
expect.objectContaining({ id: 'user4' }),
|
|
298
|
+
expect.objectContaining({ id: 'user6' })
|
|
299
|
+
]))
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it( 'should find documents using `contains` operator', async ()=>{
|
|
303
|
+
const colleague2 = new TestUser( 'colleague2' )
|
|
304
|
+
const docs = await model.find().where( 'colleagues', 'contains', colleague2 ).get()
|
|
305
|
+
|
|
306
|
+
expect( docs ).toHaveLength( 2 )
|
|
307
|
+
expect( docs ).toEqual([
|
|
308
|
+
expect.objectContaining({ id: 'user4' }),
|
|
309
|
+
expect.objectContaining({ id: 'user6' })
|
|
310
|
+
])
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe( 'Derived classes should fit on parent collection', ()=>{
|
|
316
|
+
|
|
317
|
+
it( 'should save derived object in parent collection', async ()=>{
|
|
318
|
+
const derived = new DerivedUser()
|
|
319
|
+
derived.name = { firstName: 'Fulanito', lastName: 'Derived' }
|
|
320
|
+
derived.salary = 3900
|
|
321
|
+
|
|
322
|
+
await model.save( derived )
|
|
323
|
+
|
|
324
|
+
const newUser = await model.findById( derived.id ) as DerivedUser
|
|
325
|
+
expect( newUser ).toBeInstanceOf( DerivedUser )
|
|
326
|
+
expect( newUser.salary ).toBe( 3900 )
|
|
327
|
+
expect( newUser.className ).toEqual( 'DerivedUser' )
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe( 'References to documents', ()=>{
|
|
332
|
+
let ref1: SubClass, ref2: SubClass
|
|
333
|
+
|
|
334
|
+
beforeEach( async ()=>{
|
|
335
|
+
testUser.documentRef = new SubClass()
|
|
336
|
+
testUser.documentRef.year = 2045
|
|
337
|
+
ref1 = new SubClass(); ref1.year = 2081
|
|
338
|
+
ref2 = new SubClass(); ref2.year = 2082
|
|
339
|
+
testUser.manyRefs.push( ref1 )
|
|
340
|
+
testUser.manyRefs.push( ref2 )
|
|
341
|
+
testUser.derived = new DerivedUser()
|
|
342
|
+
testUser.derived!.salary = 1350
|
|
343
|
+
testUser.manyDerived = [ new DerivedUser(), new DerivedUser() ]
|
|
344
|
+
testUser.manyDerived![ 0 ]!.salary = 990
|
|
345
|
+
testUser.manyDerived![ 1 ]!.salary = 1990
|
|
346
|
+
|
|
347
|
+
await model.save( testUser )
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it( 'should save a document as a reference', async ()=>{
|
|
351
|
+
const subClassModel = Store.getModel( 'SubClass' )
|
|
352
|
+
expect( subClassModel ).toBeDefined()
|
|
353
|
+
|
|
354
|
+
const newDocument = await subClassModel.findById( testUser.documentRef!.id ) as SubClass
|
|
355
|
+
|
|
356
|
+
expect( newDocument ).toBeInstanceOf( SubClass )
|
|
357
|
+
expect( newDocument.year ).toBe( 2045 )
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it( 'should read a swallow document reference', async ()=>{
|
|
361
|
+
const loadedUser = await model.findById( testUser.id )
|
|
362
|
+
|
|
363
|
+
expect( loadedUser?.documentRef ).toBeInstanceOf( SubClass )
|
|
364
|
+
expect( loadedUser?.documentRef?.id ).toBeDefined()
|
|
365
|
+
expect( loadedUser?.documentRef?.year ).toBeUndefined()
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it( 'should fill data of swallow document reference', async ()=>{
|
|
369
|
+
const loadedUser = await model.findById( testUser.id )
|
|
370
|
+
|
|
371
|
+
await Store.populate( loadedUser!.documentRef! )
|
|
372
|
+
expect( loadedUser?.documentRef?.id ).toBeDefined()
|
|
373
|
+
expect( loadedUser?.documentRef?.year ).toBe( 2045 )
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
it( 'should save and array of references', async ()=>{
|
|
378
|
+
const subClassModel = Store.getModel( 'SubClass' )
|
|
379
|
+
|
|
380
|
+
const newDocument = await subClassModel.findById( testUser.documentRef!.id ) as SubClass
|
|
381
|
+
|
|
382
|
+
expect( newDocument ).toBeInstanceOf( SubClass )
|
|
383
|
+
expect( newDocument.year ).toBe( 2045 )
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it( 'should read an array of references', async ()=>{
|
|
387
|
+
const loadedUser = await model.findById( testUser.id )
|
|
388
|
+
|
|
389
|
+
expect( loadedUser?.manyRefs ).toHaveLength( 2 )
|
|
390
|
+
expect( loadedUser?.manyRefs[0] ).toBeInstanceOf( SubClass )
|
|
391
|
+
expect( loadedUser?.manyRefs[0]?.id ).toEqual( testUser.manyRefs[0]!.id )
|
|
392
|
+
expect( loadedUser?.manyRefs[0]?.year ).toBeUndefined()
|
|
393
|
+
expect( loadedUser?.manyRefs[1] ).toBeInstanceOf( SubClass )
|
|
394
|
+
expect( loadedUser?.manyRefs[1]?.id ).toEqual( testUser.manyRefs[1]!.id )
|
|
395
|
+
expect( loadedUser?.manyRefs[1]?.year ).toBeUndefined()
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it( 'should fill array of refs', async ()=>{
|
|
399
|
+
const loadedUser = await model.findById( testUser.id )
|
|
400
|
+
await Store.populate( loadedUser!.manyRefs )
|
|
401
|
+
|
|
402
|
+
expect( loadedUser?.manyRefs[ 0 ]?.year ).toBe( 2081 )
|
|
403
|
+
expect( loadedUser?.manyRefs[ 1 ]?.year ).toBe( 2082 )
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it( 'should save a reference when declared @persistentAt', async ()=>{
|
|
407
|
+
const loadedUser = await model.findById( testUser.id )
|
|
408
|
+
|
|
409
|
+
expect( loadedUser?.derived?.id ).toEqual( testUser.derived!.id )
|
|
410
|
+
expect( loadedUser?.derived?.salary ).toBeUndefined()
|
|
411
|
+
|
|
412
|
+
await Store.populate( loadedUser!.derived! )
|
|
413
|
+
|
|
414
|
+
expect( loadedUser?.derived?.salary ).toBe( 1350 )
|
|
415
|
+
expect( loadedUser?.derived?.id ).toBe( testUser.derived!.id )
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it( 'should populate from special collection when declared with @persistentRefAt', async ()=>{
|
|
419
|
+
const loadedUser = await model.findById( 'user6' )
|
|
420
|
+
await Store.populate( loadedUser!.derived! )
|
|
421
|
+
|
|
422
|
+
expect( loadedUser?.derived?.salary ).toBe( 2800 )
|
|
423
|
+
expect( loadedUser?.derived?.id ).toBe( 'user4' )
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it( 'should save a reference when declared @persistentAt as array', async ()=>{
|
|
427
|
+
const loadedUser = await model.findById( testUser.id )
|
|
428
|
+
|
|
429
|
+
expect( loadedUser?.manyDerived?.[0]?.id ).toEqual( testUser.manyDerived![0]!.id )
|
|
430
|
+
expect( loadedUser?.manyDerived?.[0]?.salary ).toBeUndefined()
|
|
431
|
+
expect( loadedUser?.manyDerived?.[1]?.salary ).toBeUndefined()
|
|
432
|
+
|
|
433
|
+
await Store.populate( loadedUser!.manyDerived! )
|
|
434
|
+
|
|
435
|
+
expect( loadedUser?.manyDerived?.[0]?.salary ).toBe( 990 )
|
|
436
|
+
expect( loadedUser?.manyDerived?.[0]?.id ).toBe( testUser.manyDerived![0]!.id )
|
|
437
|
+
expect( loadedUser?.manyDerived?.[1]?.salary ).toBe( 1990 )
|
|
438
|
+
expect( loadedUser?.manyDerived?.[1]?.id ).toBe( testUser.manyDerived?.[1]?.id )
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it( 'should not overwrite not filled ref in collection', async ()=>{
|
|
442
|
+
const loadedUser = await model.findById( 'user6' )
|
|
443
|
+
await model.save( loadedUser! )
|
|
444
|
+
const refInCollection = await model.findById<DerivedUser>( 'user4' )
|
|
445
|
+
|
|
446
|
+
expect( refInCollection?.salary ).toBe( 2800 )
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it( 'should save loaded ref with assigned new instance', async ()=>{
|
|
450
|
+
const loadedUser = await model.findById( 'user6' )
|
|
451
|
+
loadedUser!.derived = new DerivedUser()
|
|
452
|
+
loadedUser!.derived!.salary = 345
|
|
453
|
+
await model.save( loadedUser! )
|
|
454
|
+
|
|
455
|
+
const refInCollection = await model.findById<DerivedUser>( loadedUser!.derived.id )
|
|
456
|
+
expect( refInCollection?.salary ).toBe( 345 )
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it( 'should save loaded ref with modified ref data', async ()=>{
|
|
460
|
+
const loadedUser = await model.findById( 'user6' )
|
|
461
|
+
await Store.populate( loadedUser!.derived! )
|
|
462
|
+
loadedUser!.derived!.salary = 1623
|
|
463
|
+
await model.save( loadedUser! )
|
|
464
|
+
|
|
465
|
+
const refInCollection = await model.findById<DerivedUser>( 'user4' )
|
|
466
|
+
expect( refInCollection?.salary ).toBe( 1623 )
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
describe( 'Operations on queries', ()=>{
|
|
471
|
+
it( 'should limit the result set', async ()=>{
|
|
472
|
+
const unlimited = await model.find().get()
|
|
473
|
+
const limited = await model.find().limit( 2 ).get()
|
|
474
|
+
|
|
475
|
+
expect( unlimited.length ).not.toBe( limited.length )
|
|
476
|
+
expect( limited ).toHaveLength( 2 )
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it( 'should sort ascending the result set', async ()=>{
|
|
480
|
+
const docs = await model.find().orderBy( 'age' ).get()
|
|
481
|
+
|
|
482
|
+
expect( docs[ 0 ]?.id ).toEqual( 'user2' )
|
|
483
|
+
expect( docs[ 1 ]?.id ).toEqual( 'user1' )
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it( 'should sort descending the result set', async ()=>{
|
|
487
|
+
const docs = await model.find().orderBy( 'age', 'desc' ).get()
|
|
488
|
+
|
|
489
|
+
expect( docs[ 0 ]?.id ).toEqual( 'user3' )
|
|
490
|
+
expect( docs[ 1 ]?.id ).toEqual( 'user5' )
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it( 'should sort by deep property path', async ()=>{
|
|
494
|
+
const docs = await model.find().orderByDeepProp( 'name.firstName', 'desc' ).get()
|
|
495
|
+
|
|
496
|
+
expect( docs[ 0 ]?.id ).toEqual( 'user6' )
|
|
497
|
+
expect( docs[ 1 ]?.id ).toEqual( 'user5' )
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it( 'should sort by swallow property path', async ()=>{
|
|
501
|
+
const docs = await model.find().orderByDeepProp( 'age' ).get()
|
|
502
|
+
|
|
503
|
+
expect( docs[ 0 ]?.id ).toEqual( 'user2' )
|
|
504
|
+
expect( docs[ 1 ]?.id ).toEqual( 'user1' )
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it( 'should count documents in collection', async ()=>{
|
|
508
|
+
expect( await model.find().count() ).toBe( 6 )
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
describe( 'Data Cursors', ()=>{
|
|
513
|
+
beforeEach( async ()=>{
|
|
514
|
+
await model.find().get( 2 )
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it( 'should get next result set', async ()=>{
|
|
518
|
+
const docs = await model.next()
|
|
519
|
+
const mockDataArr = Object.values( mockData.TestUser )
|
|
520
|
+
|
|
521
|
+
expect( docs ).toHaveLength( 2 )
|
|
522
|
+
expect( docs[0]?.id ).toEqual( mockDataArr[2]!.id )
|
|
523
|
+
expect( docs[ 0 ]?.id ).toEqual( 'user3' )
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it( 'should not go beyond the end of result set', async ()=>{
|
|
527
|
+
await model.next()
|
|
528
|
+
await model.next()
|
|
529
|
+
const docs = await model.next()
|
|
530
|
+
expect( docs ).toHaveLength( 0 )
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
describe( 'SubCollections', ()=>{
|
|
537
|
+
let model: Model<SubClass>
|
|
538
|
+
|
|
539
|
+
beforeEach(()=>{
|
|
540
|
+
model = Store.getModelForSubCollection( testUser, 'SubClass' )
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it( 'should retrieve from subcollection', async ()=>{
|
|
544
|
+
const subClass = new SubClass()
|
|
545
|
+
subClass.year = 3452
|
|
546
|
+
|
|
547
|
+
await model.save( subClass )
|
|
548
|
+
|
|
549
|
+
const loaded = await model.findById( subClass.id )
|
|
550
|
+
|
|
551
|
+
expect( loaded?.year ).toBe( 3452 )
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { and, collection, connectFirestoreEmulator, deleteDoc, doc, DocumentData, getCountFromServer, getDoc, getDocs, limit, or, orderBy, Query, query, QueryDocumentSnapshot, QueryFieldFilterConstraint, QueryNonFilterConstraint, startAfter, where, WhereFilterOp, writeBatch } from 'firebase/firestore'
|
|
2
|
+
import { Collections, DataSource, DocumentObject, QueryObject, QueryOperator } from 'entropic-bond'
|
|
3
|
+
import { EmulatorConfig, FirebaseHelper, FirebaseQuery } from '../firebase-helper'
|
|
4
|
+
|
|
5
|
+
interface ConstraintsContainer {
|
|
6
|
+
andConstraints: QueryFieldFilterConstraint[]
|
|
7
|
+
orConstraints: QueryFieldFilterConstraint[]
|
|
8
|
+
nonFilterConstraints: QueryNonFilterConstraint[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class FirebaseDatasource extends DataSource {
|
|
12
|
+
constructor( emulator?: EmulatorConfig ) {
|
|
13
|
+
super()
|
|
14
|
+
if ( emulator ) FirebaseHelper.useEmulator( emulator )
|
|
15
|
+
|
|
16
|
+
if ( FirebaseHelper.emulator?.emulate ) {
|
|
17
|
+
const { host, firestorePort } = FirebaseHelper.emulator
|
|
18
|
+
connectFirestoreEmulator( FirebaseHelper.instance.firestore(), host, firestorePort )
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
findById( id: string, collectionName: string ): Promise< DocumentObject > {
|
|
23
|
+
const db = FirebaseHelper.instance.firestore()
|
|
24
|
+
|
|
25
|
+
return new Promise<DocumentObject>( async resolve => {
|
|
26
|
+
try {
|
|
27
|
+
const docSnap = await getDoc( doc( db, collectionName, id ) )
|
|
28
|
+
resolve( docSnap.data() as DocumentObject )
|
|
29
|
+
}
|
|
30
|
+
catch( error ) {
|
|
31
|
+
console.log( error )
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
save( collections: Collections ): Promise< void > {
|
|
38
|
+
const db = FirebaseHelper.instance.firestore()
|
|
39
|
+
const batch = writeBatch( db )
|
|
40
|
+
|
|
41
|
+
Object.entries( collections ).forEach(([ collectionName, collection ]) => {
|
|
42
|
+
collection?.forEach( document => {
|
|
43
|
+
const ref = doc( db, collectionName, document.id )
|
|
44
|
+
batch.set( ref, document )
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return batch.commit()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
find( queryObject: QueryObject<DocumentObject>, collectionName: string ): Promise< DocumentObject[] > {
|
|
52
|
+
const query = this.queryObjectToQueryConstraints( queryObject, collectionName )
|
|
53
|
+
return this.getFromQuery( query )
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async count( queryObject: QueryObject<DocumentObject>, collectionName: string ): Promise<number> {
|
|
57
|
+
const query = this.queryObjectToQueryConstraints( queryObject, collectionName )
|
|
58
|
+
|
|
59
|
+
const snapShot = await getCountFromServer( query )
|
|
60
|
+
return snapShot.data().count
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
delete( id: string, collectionName: string ): Promise< void > {
|
|
64
|
+
const db = FirebaseHelper.instance.firestore()
|
|
65
|
+
|
|
66
|
+
return deleteDoc( doc( db, collectionName, id ) )
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
next( maxDocs?: number ): Promise< DocumentObject[] > {
|
|
70
|
+
if( !this._lastConstraints || !this._lastCollectionName ) throw new Error('You should perform a query prior to using method next')
|
|
71
|
+
|
|
72
|
+
const db = FirebaseHelper.instance.firestore()
|
|
73
|
+
this._lastLimit = maxDocs || this._lastLimit
|
|
74
|
+
|
|
75
|
+
const constraints = this._lastConstraints.nonFilterConstraints.concat(
|
|
76
|
+
limit( this._lastLimit ),
|
|
77
|
+
startAfter( this._lastDocRetrieved )
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return this.getFromQuery( query( collection( db, this._lastCollectionName ), ...constraints ) )
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// prev should be used with next in reverse order
|
|
84
|
+
// prev( limit?: number ): Promise< DocumentObject[] > {
|
|
85
|
+
// }
|
|
86
|
+
|
|
87
|
+
private queryObjectToQueryConstraints( queryObject: QueryObject<DocumentObject>, collectionName: string ): Query {
|
|
88
|
+
const db = FirebaseHelper.instance.firestore()
|
|
89
|
+
const andConstraints: QueryFieldFilterConstraint[] = []
|
|
90
|
+
const orConstraints: QueryFieldFilterConstraint[] = []
|
|
91
|
+
const nonFilterConstraints: QueryNonFilterConstraint[] = []
|
|
92
|
+
|
|
93
|
+
DataSource.toPropertyPathOperations( queryObject.operations as any ).forEach( operation => {
|
|
94
|
+
const operator = this.toFirebaseOperator( operation.operator )
|
|
95
|
+
if ( operation.aggregate ) orConstraints.push( where( operation.property, operator, operation.value ) )
|
|
96
|
+
else andConstraints.push( where( operation.property, operator, operation.value ) )
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if ( queryObject.sort?.propertyName ) {
|
|
100
|
+
nonFilterConstraints.push( orderBy( queryObject.sort.propertyName, queryObject.sort.order ) )
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this._lastConstraints = {
|
|
104
|
+
orConstraints,
|
|
105
|
+
andConstraints,
|
|
106
|
+
nonFilterConstraints
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this._lastCollectionName = collectionName
|
|
110
|
+
|
|
111
|
+
if( queryObject.limit ) {
|
|
112
|
+
this._lastLimit = queryObject.limit
|
|
113
|
+
nonFilterConstraints.push( limit( queryObject.limit ) )
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return query( collection( db, collectionName ), or( ...orConstraints, and( ...andConstraints ) ), ...nonFilterConstraints )
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
toFirebaseOperator( operator: QueryOperator ): WhereFilterOp {
|
|
120
|
+
switch( operator ) {
|
|
121
|
+
case '==':
|
|
122
|
+
case '!=':
|
|
123
|
+
case '<':
|
|
124
|
+
case '<=':
|
|
125
|
+
case '>':
|
|
126
|
+
case '>=': return operator
|
|
127
|
+
case 'contains': return 'array-contains'
|
|
128
|
+
case 'containsAny': return 'array-contains-any'
|
|
129
|
+
default: return operator
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private getFromQuery( query: FirebaseQuery ) {
|
|
134
|
+
return new Promise< DocumentObject[] >( async resolve => {
|
|
135
|
+
const doc = await getDocs( query )
|
|
136
|
+
this._lastDocRetrieved = doc.docs[ doc.docs.length-1 ]
|
|
137
|
+
|
|
138
|
+
resolve( doc.docs.map( doc => doc.data() as DocumentObject ) )
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private _lastDocRetrieved: QueryDocumentSnapshot<DocumentData> | undefined
|
|
143
|
+
private _lastConstraints: ConstraintsContainer | undefined
|
|
144
|
+
private _lastLimit: number = 0
|
|
145
|
+
private _lastCollectionName: string | undefined
|
|
146
|
+
}
|