@entropic-bond/firebase 1.12.1 → 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.
Files changed (48) hide show
  1. package/.firebaserc +5 -0
  2. package/.github/workflows/release.yml +27 -0
  3. package/CHANGELOG.md +331 -0
  4. package/firebase.json +30 -0
  5. package/firestore.indexes.json +4 -0
  6. package/firestore.rules +8 -0
  7. package/functions/package-lock.json +2344 -0
  8. package/functions/package.json +26 -0
  9. package/functions/src/index.ts +33 -0
  10. package/functions/tsconfig.json +19 -0
  11. package/package.json +15 -12
  12. package/src/auth/firebase-auth.spec.ts +90 -0
  13. package/src/auth/firebase-auth.ts +212 -0
  14. package/src/cloud-functions/firebase-cloud-functions.spec.ts +47 -0
  15. package/src/cloud-functions/firebase-cloud-functions.ts +25 -0
  16. package/src/cloud-storage/firebase-cloud-storage.spec.ts +135 -0
  17. package/src/cloud-storage/firebase-cloud-storage.ts +67 -0
  18. package/src/firebase-helper.ts +92 -0
  19. package/src/index.ts +5 -0
  20. package/src/mocks/mock-data.json +148 -0
  21. package/src/mocks/test-user.ts +121 -0
  22. package/src/store/firebase-datasource.spec.ts +555 -0
  23. package/src/store/firebase-datasource.ts +146 -0
  24. package/storage.rules +8 -0
  25. package/tsconfig-build.json +7 -0
  26. package/tsconfig.json +30 -0
  27. package/vite.config.ts +23 -0
  28. package/lib/auth/firebase-auth.d.ts +0 -20
  29. package/lib/auth/firebase-auth.js +0 -186
  30. package/lib/auth/firebase-auth.js.map +0 -1
  31. package/lib/cloud-functions/firebase-cloud-functions.d.ts +0 -7
  32. package/lib/cloud-functions/firebase-cloud-functions.js +0 -26
  33. package/lib/cloud-functions/firebase-cloud-functions.js.map +0 -1
  34. package/lib/cloud-storage/firebase-cloud-storage.d.ts +0 -10
  35. package/lib/cloud-storage/firebase-cloud-storage.js +0 -66
  36. package/lib/cloud-storage/firebase-cloud-storage.js.map +0 -1
  37. package/lib/firebase-helper.d.ts +0 -38
  38. package/lib/firebase-helper.js +0 -57
  39. package/lib/firebase-helper.js.map +0 -1
  40. package/lib/index.d.ts +0 -5
  41. package/lib/index.js +0 -22
  42. package/lib/index.js.map +0 -1
  43. package/lib/mocks/test-user.d.ts +0 -49
  44. package/lib/mocks/test-user.js +0 -134
  45. package/lib/mocks/test-user.js.map +0 -1
  46. package/lib/store/firebase-datasource.d.ts +0 -19
  47. package/lib/store/firebase-datasource.js +0 -115
  48. package/lib/store/firebase-datasource.js.map +0 -1
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "functions",
3
+ "scripts": {
4
+ "build": "tsc",
5
+ "build:watch": "tsc --watch",
6
+ "serve": "npm run build && firebase emulators:start --only functions",
7
+ "shell": "npm run build && firebase functions:shell",
8
+ "start": "npm run shell",
9
+ "deploy": "firebase deploy --only functions",
10
+ "logs": "firebase functions:log",
11
+ "install-build": "npm i && npm run build",
12
+ "postinstall": "npm run build"
13
+ },
14
+ "engines": {
15
+ "node": "20"
16
+ },
17
+ "main": "lib/index.js",
18
+ "dependencies": {
19
+ "firebase-admin": "^12.0.0",
20
+ "firebase-functions": "^4.7.0"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.3.3"
24
+ },
25
+ "private": true
26
+ }
@@ -0,0 +1,33 @@
1
+ import * as _functions from 'firebase-functions'
2
+ import admin from 'firebase-admin'
3
+ import { persistent, Persistent, PersistentObject, registerPersistentClass } from 'entropic-bond'
4
+
5
+ @registerPersistentClass( 'ParamWrapper' )
6
+ export class ParamWrapper extends Persistent {
7
+ constructor( a?: string, b?: number ) {
8
+ super()
9
+ this._a = a
10
+ this._b = b
11
+ }
12
+ @persistent _a: string
13
+ @persistent _b: number
14
+ }
15
+
16
+
17
+ admin.initializeApp()
18
+ const functions = _functions.region('europe-west1')
19
+
20
+ export const test = functions.https.onRequest((_req, res) => {
21
+ res.send('Hello from Firebase!')
22
+ })
23
+
24
+ export const testCallablePersistent = functions.https.onCall(
25
+ ( param: PersistentObject<ParamWrapper> ) => {
26
+ Persistent.registerFactory( 'ParamWrapper', ParamWrapper )
27
+ return param
28
+ }
29
+ )
30
+
31
+ export const testCallablePlain = functions.https.onCall(
32
+ ( param: string ) => param.length
33
+ )
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "esModuleInterop": true,
7
+ "sourceMap": true,
8
+ "outDir": "lib",
9
+ "resolveJsonModule": true,
10
+ "experimentalDecorators": true,
11
+ "lib": [
12
+ "ES2021", "WebWorker"
13
+ ]
14
+ },
15
+ "compileOnSave": false,
16
+ "include": [
17
+ "src"
18
+ ]
19
+ }
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@entropic-bond/firebase",
3
- "version": "1.12.1",
4
3
  "type": "module",
4
+ "version": "1.13.0",
5
+ "description": "Firebase plugins for Entropic Bond",
6
+ "main": "./lib/entropic-bond-firebase.umd.cjs",
7
+ "module": "./lib/entropic-bond-firebase.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./lib/index.js",
11
+ "require": "./lib/entropic-bond-firebase.umd.cjs",
12
+ "types": "./lib/index.d.ts"
13
+ }
14
+ },
5
15
  "publishConfig": {
6
16
  "access": "public",
7
17
  "branches": [
@@ -17,18 +27,10 @@
17
27
  "@semantic-release/github"
18
28
  ]
19
29
  },
20
- "description": "Firebase plugins for Entropic Bond",
21
- "main": "lib/index.js",
22
- "types": "lib/index.d.ts",
23
- "files": [
24
- "lib"
25
- ],
26
30
  "scripts": {
27
31
  "test": "npm run install-functions-dependencies && firebase emulators:exec --project demo-test vitest",
28
- "build": "npm run build-cjs",
32
+ "build": "tsc -p tsconfig-build.json && vite build",
29
33
  "prepare": "npm run build",
30
- "build-ts": "cp -r ./src/ ./lib",
31
- "build-cjs": "tsc -p tsconfig-cjs.json",
32
34
  "emulators": "firebase emulators:start --project demo-test",
33
35
  "install-functions-dependencies": "cd functions && npm ci"
34
36
  },
@@ -52,13 +54,14 @@
52
54
  "@types/node": "^20.11.16",
53
55
  "git-branch-is": "^4.0.0",
54
56
  "husky": "^9.0.10",
55
- "semantic-release": "^23.0.0",
57
+ "semantic-release": "^23.0.2",
56
58
  "typescript": "^5.3.3",
59
+ "vite-plugin-dts": "^3.7.2",
57
60
  "vitest": "^1.2.2",
58
61
  "xhr2": "^0.2.1"
59
62
  },
60
63
  "dependencies": {
61
- "entropic-bond": "^1.49.0",
64
+ "entropic-bond": "^1.50.2",
62
65
  "firebase": "^10.8.0"
63
66
  }
64
67
  }
@@ -0,0 +1,90 @@
1
+ import { Auth } from 'entropic-bond'
2
+ import { FirebaseHelper } from '../firebase-helper'
3
+ import { FirebaseAuth } from './firebase-auth'
4
+
5
+ // NOTE: Firebase auth emulator requires a modification of the code to test which
6
+ // violates testing best practices. Therefore, this test is disabled.
7
+
8
+ describe( 'Firebase Auth', ()=>{
9
+ it( 'should pass', ()=>{
10
+ expect( true ).toBe( true )
11
+ })
12
+
13
+ // let authChangeSpy = vi.fn()
14
+ // FirebaseHelper.setFirebaseConfig({
15
+ // projectId: "demo-test",
16
+ // })
17
+ // FirebaseHelper.useEmulator({ authPort: 9099 })
18
+
19
+ // beforeEach(()=>{
20
+ // Auth.registerAuthService( new FirebaseAuth() )
21
+ // Auth.instance.onAuthStateChange( authChangeSpy )
22
+ // })
23
+
24
+ // it( 'should emulate sign-up', async ()=>{
25
+ // const userCredential = await Auth.instance.signUp({
26
+ // authProvider: 'google',
27
+ // email: 'test@test.com',
28
+ // password: 'password'
29
+ // })
30
+
31
+ // expect( userCredential.email ).toEqual( 'test@test.com' )
32
+ // expect( authChangeSpy ).toHaveBeenCalledWith( userCredential )
33
+ // })
34
+
35
+ // it( 'should emulate failed sign-up', async ()=>{
36
+ // try {
37
+ // var userCredential = await Auth.instance.signUp({
38
+ // authProvider: 'fail',
39
+ // email: 'test@test.com',
40
+ // password: 'password'
41
+ // })
42
+ // }
43
+ // catch {}
44
+
45
+ // expect( userCredential ).toBeUndefined()
46
+ // expect( authChangeSpy ).toHaveBeenCalledWith( undefined )
47
+ // })
48
+
49
+ // it( 'should login with fake registered user', async ()=>{
50
+ // const userCredentials = await Auth.instance.login({
51
+ // email: 'fakeUser@test.com',
52
+ // password: 'password',
53
+ // authProvider: 'google'
54
+ // })
55
+
56
+ // expect( userCredentials ).toEqual( 'fakeUseCredentials' )
57
+ // expect( authChangeSpy ).toHaveBeenCalledWith( 'fakeUseCredentials' )
58
+ // })
59
+
60
+ // it( 'should fail login with email auth provider if does not match fake user credentials', async ()=>{
61
+ // try {
62
+ // var userCredentials = await Auth.instance.login({
63
+ // email: 'test@test.com',
64
+ // password: 'password',
65
+ // authProvider: 'email'
66
+ // })
67
+ // } catch {}
68
+
69
+ // expect( userCredentials ).toEqual( undefined )
70
+ // expect( authChangeSpy ).toHaveBeenCalledWith( undefined )
71
+ // })
72
+
73
+ // it( 'should NOT fail login with non email auth provider even if does not match fake user credentials', async ()=>{
74
+ // const userCredentials = await Auth.instance.login({
75
+ // email: 'test@test.com',
76
+ // password: 'password',
77
+ // authProvider: 'google'
78
+ // })
79
+
80
+ // expect( userCredentials.email ).toEqual( 'test@test.com' )
81
+ // expect( authChangeSpy ).toHaveBeenCalledWith( undefined )
82
+ // })
83
+
84
+ // it( 'should logout', async ()=>{
85
+ // await Auth.instance.logout()
86
+
87
+ // expect( authChangeSpy ).toHaveBeenCalledWith( undefined )
88
+ // })
89
+
90
+ })
@@ -0,0 +1,212 @@
1
+ import { AuthProvider, SignData, UserCredentials, AuthService, RejectedCallback, ResovedCallback, AuthErrorCode, camelCase } from 'entropic-bond'
2
+ import { connectAuthEmulator, createUserWithEmailAndPassword, FacebookAuthProvider, GoogleAuthProvider, linkWithPopup, sendEmailVerification, signInAnonymously, signInWithEmailAndPassword, signInWithPopup, TwitterAuthProvider, updateProfile, unlink, User, UserCredential, sendPasswordResetEmail } from 'firebase/auth'
3
+ import { EmulatorConfig, FirebaseHelper } from '../firebase-helper'
4
+
5
+ interface CredentialProviders {
6
+ [ name: string ]: ( signData: SignData ) => Promise<UserCredential>
7
+ }
8
+
9
+ const providerFactory = {
10
+ 'twitter': () => new TwitterAuthProvider(),
11
+ 'facebook': () => new FacebookAuthProvider(),
12
+ 'google': () => new GoogleAuthProvider()
13
+ }
14
+
15
+ export class FirebaseAuth extends AuthService {
16
+ constructor( emulator?: EmulatorConfig ) {
17
+ super()
18
+ if ( emulator ) FirebaseHelper.useEmulator( emulator )
19
+
20
+ if ( FirebaseHelper.emulator?.emulate ) {
21
+ const { host, authPort } = FirebaseHelper.emulator
22
+ if ( !host || !authPort ) throw new Error( `You should define a host and an auth emulator port to use the emulator` )
23
+
24
+ connectAuthEmulator( FirebaseHelper.instance.auth(), `http://${ host }:${ authPort }` )
25
+ }
26
+
27
+ this.registerCredentialProviders()
28
+ }
29
+
30
+ signUp<T extends {}>( signData: SignData ): Promise<UserCredentials<T>> {
31
+ const { authProvider, verificationLink } = signData
32
+
33
+ if ( authProvider.slice( 0, 5 ) === 'email' ) {
34
+ return new Promise<UserCredentials<T>>( async ( resolve: ResovedCallback<T>, reject: RejectedCallback ) => {
35
+ try {
36
+ const credentialFactory = this.credentialProviders[ 'email-sign-up' ]
37
+ if ( !credentialFactory ) throw new Error( `The provider ${ authProvider } is not registered` )
38
+
39
+ const userCredentials = await credentialFactory( signData )
40
+
41
+ if ( signData.name ) {
42
+ await updateProfile( userCredentials.user, {
43
+ displayName: signData.name
44
+ })
45
+ }
46
+
47
+ if ( verificationLink ) {
48
+ await sendEmailVerification( userCredentials.user, {
49
+ url: verificationLink
50
+ })
51
+ }
52
+
53
+ resolve( await this.toUserCredentials( userCredentials.user ) )
54
+ }
55
+ catch( error ) {
56
+ reject({
57
+ code: camelCase( error.code.slice( 5 ) ) as AuthErrorCode,
58
+ message: error.message
59
+ })
60
+ }
61
+ })
62
+ }
63
+ else return this.login( signData )
64
+ }
65
+
66
+ login<T extends {}>( signData: SignData ): Promise<UserCredentials<T>> {
67
+ const { authProvider } = signData
68
+
69
+ return new Promise<UserCredentials<T>>( async ( resolve: ResovedCallback<T>, reject: RejectedCallback ) => {
70
+ try {
71
+ const credentialFactory = this.credentialProviders[ authProvider ]
72
+ if ( !credentialFactory ) throw new Error( `The provider ${ authProvider } is not registered` )
73
+ const userCredentials = await credentialFactory( signData )
74
+ resolve( await this.toUserCredentials<T>( userCredentials.user ) )
75
+ }
76
+ catch( error ) {
77
+ reject({
78
+ code: error.code === 400? 'missingPassword' : camelCase( error.code.slice( 5 )) as AuthErrorCode,
79
+ message: error.message
80
+ })
81
+ }
82
+ })
83
+ }
84
+
85
+ logout(): Promise<void> {
86
+ return FirebaseHelper.instance.auth().signOut()
87
+ }
88
+
89
+ resetEmailPassword( email: string ) {
90
+ return new Promise<void>( async ( resolve, reject ) => {
91
+ try {
92
+ await sendPasswordResetEmail( FirebaseHelper.instance.auth(), email )
93
+ resolve()
94
+ }
95
+ catch( error ) {
96
+ reject({
97
+ code: camelCase( error.code.slice( 5 ) ) as AuthErrorCode,
98
+ message: error.message
99
+ })
100
+ }
101
+ })
102
+ }
103
+
104
+ resendVerificationEmail( email: string, password: string, verificationLink: string ): Promise<void> {
105
+ return new Promise<void>( async ( resolve, reject ) => {
106
+ try {
107
+ await signInWithEmailAndPassword( FirebaseHelper.instance.auth(), email, password )
108
+ const user = FirebaseHelper.instance.auth().currentUser
109
+ if ( !user ) {
110
+ reject({
111
+ code: 'userNotFound',
112
+ message: `There is no logged in user`
113
+ })
114
+ return
115
+ }
116
+
117
+ await sendEmailVerification( user, {
118
+ url: verificationLink
119
+ })
120
+ resolve()
121
+ }
122
+ catch( error ) {
123
+ reject({
124
+ code: camelCase( error.code.slice( 5 ) ) as AuthErrorCode,
125
+ message: verificationLink
126
+ })
127
+ }
128
+ })
129
+ }
130
+
131
+ override refreshToken(): Promise<void> {
132
+ return FirebaseHelper.instance.auth().currentUser?.getIdToken( true ) as unknown as Promise<void>
133
+ }
134
+
135
+ onAuthStateChange<T extends {}>( onChange: (userCredentials: UserCredentials<T> | undefined) => void ) {
136
+ FirebaseHelper.instance.auth().onAuthStateChanged( async credentials =>{
137
+ onChange( credentials? await this.toUserCredentials( credentials ) : undefined )
138
+ })
139
+ }
140
+
141
+ linkAdditionalProvider( provider: AuthProvider ): Promise<unknown> {
142
+ const providerInstance = providerFactory[ provider ]()
143
+ const currentUser = FirebaseHelper.instance.auth().currentUser
144
+ if ( !currentUser ) throw new Error( `There is no logged in user` )
145
+
146
+ return linkWithPopup( currentUser, providerInstance )
147
+ }
148
+
149
+ unlinkProvider( provider: AuthProvider ): Promise<unknown> {
150
+ const { currentUser } = FirebaseHelper.instance.auth()
151
+ if ( !currentUser ) throw new Error( `There is no logged in user` )
152
+
153
+ currentUser.providerData
154
+ return unlink( currentUser, providerFactory[ provider ]().providerId )
155
+ }
156
+
157
+ private async toUserCredentials<T extends {}>( nativeUserCredential: User ): Promise<UserCredentials<T>> {
158
+ if ( !nativeUserCredential ) throw new Error( `The user in user credentials is not defined` )
159
+
160
+ const claims = ( await nativeUserCredential.getIdTokenResult() ).claims as T
161
+
162
+ return FirebaseAuth.convertCredentials<T>( nativeUserCredential, claims )
163
+ }
164
+
165
+ static convertCredentials<T extends {}>( nativeUserCredential: User, claims: T ): UserCredentials<T> {
166
+ return ({
167
+ id: nativeUserCredential.uid,
168
+ email: nativeUserCredential.email ?? '',
169
+ name: nativeUserCredential.displayName ?? undefined,
170
+ pictureUrl: nativeUserCredential.photoURL ?? undefined,
171
+ phoneNumber: nativeUserCredential.phoneNumber ?? undefined,
172
+ emailVerified: nativeUserCredential.emailVerified,
173
+ customData: {...claims},
174
+ lastLogin: Date.now(),
175
+ creationDate: nativeUserCredential.metadata.creationTime? new Date( nativeUserCredential.metadata.creationTime ).getTime() : undefined,
176
+ })
177
+ }
178
+
179
+ registerCredentialProvider( name: string, providerFactory: ( singData: SignData ) => Promise<UserCredential> ) {
180
+ this.credentialProviders[ name ] = providerFactory
181
+ }
182
+
183
+ private registerCredentialProviders() { //TODO: refactor. Not needed anymore
184
+ this.registerCredentialProvider( 'email-sign-up', signData => {
185
+ if ( !signData.email || !signData.password ) throw new Error( `Email and password are required` )
186
+ return createUserWithEmailAndPassword( FirebaseHelper.instance.auth(), signData.email, signData.password )
187
+ })
188
+ this.registerCredentialProvider( 'email', signData => {
189
+ if ( !signData.email || !signData.password ) throw new Error( `Email and password are required` )
190
+ return signInWithEmailAndPassword( FirebaseHelper.instance.auth(), signData.email, signData.password )
191
+ })
192
+ this.registerCredentialProvider( 'google', () => signInWithPopup(
193
+ FirebaseHelper.instance.auth(), new GoogleAuthProvider()
194
+ ))
195
+ this.registerCredentialProvider( 'facebook', () => signInWithPopup(
196
+ FirebaseHelper.instance.auth(), new FacebookAuthProvider()
197
+ ))
198
+ this.registerCredentialProvider( 'twitter', () => signInWithPopup(
199
+ FirebaseHelper.instance.auth(), new TwitterAuthProvider()
200
+ ))
201
+ this.registerCredentialProvider( 'link-twitter', () => {
202
+ const currentUser = FirebaseHelper.instance.auth().currentUser
203
+ if ( !currentUser ) throw new Error( `There is no logged in user` )
204
+ return linkWithPopup( currentUser, new TwitterAuthProvider() )
205
+ })
206
+ this.registerCredentialProvider( 'anonymous', () => signInAnonymously(
207
+ FirebaseHelper.instance.auth()
208
+ ))
209
+ }
210
+
211
+ private credentialProviders: CredentialProviders = {}
212
+ }
@@ -0,0 +1,47 @@
1
+ import { CloudFunctions, Persistent, persistent, registerPersistentClass } from 'entropic-bond'
2
+ import { FirebaseHelper } from '../firebase-helper'
3
+ import { FirebaseCloudFunctions } from './firebase-cloud-functions'
4
+
5
+ @registerPersistentClass( 'ParamWrapper' )
6
+ export class ParamWrapper extends Persistent {
7
+ constructor( a?: string, b?: number ) {
8
+ super()
9
+ this._a = a
10
+ this._b = b
11
+ }
12
+ @persistent _a: string | undefined
13
+ @persistent _b: number | undefined
14
+ }
15
+
16
+ describe( 'Cloud functions', ()=>{
17
+
18
+ beforeEach(()=>{
19
+ FirebaseHelper.setFirebaseConfig({
20
+ projectId: 'demo-test',
21
+ storageBucket: 'default-bucket'
22
+ })
23
+
24
+ FirebaseHelper.useEmulator()
25
+ CloudFunctions.useCloudFunctionsService(
26
+ new FirebaseCloudFunctions( 'europe-west1', { emulate: true })
27
+ )
28
+ })
29
+
30
+ it( 'should call cloud functions with plain types', async ()=>{
31
+ const testCallablePlain = CloudFunctions.instance.getFunction<string, number>( 'testCallablePlain' )
32
+ const result = await testCallablePlain( 'Hello' )
33
+
34
+ expect( result ).toBe( 5 )
35
+ })
36
+
37
+ it( 'should call cloud function for Persistent', async ()=>{
38
+ const testCallablePersistent = CloudFunctions.instance.getFunction<ParamWrapper, ParamWrapper>( 'testCallablePersistent' )
39
+ const paramWrapper = new ParamWrapper( 'test', 30 )
40
+
41
+ const a = paramWrapper.toObject()
42
+
43
+ const result = await testCallablePersistent( paramWrapper )
44
+ expect( result._a ).toEqual( 'test' )
45
+ expect( result._b ).toEqual( 30 )
46
+ })
47
+ })
@@ -0,0 +1,25 @@
1
+ import { CloudFunction, CloudFunctionsService } from 'entropic-bond'
2
+ import { connectFunctionsEmulator, httpsCallable } from 'firebase/functions'
3
+ import { EmulatorConfig, FirebaseHelper } from '../firebase-helper'
4
+
5
+ export class FirebaseCloudFunctions implements CloudFunctionsService {
6
+ constructor( region?: string, emulator?: Partial<EmulatorConfig> ) {
7
+ if ( region ) FirebaseHelper.setRegion( region )
8
+ if ( emulator ) FirebaseHelper.useEmulator( emulator )
9
+
10
+ if ( FirebaseHelper.emulator?.emulate ) {
11
+ const { host, functionsPort } = FirebaseHelper.emulator
12
+ connectFunctionsEmulator( FirebaseHelper.instance.functions(), host, functionsPort )
13
+ }
14
+
15
+ }
16
+
17
+ retrieveFunction<P, R>( cloudFunction: string ): CloudFunction<P, R> {
18
+ return httpsCallable<P,R>( FirebaseHelper.instance.functions(), cloudFunction ) as unknown as CloudFunction<P, R>
19
+ }
20
+
21
+ async callFunction<P, R>( func: CloudFunction<P, R>, params: P ): Promise<R> {
22
+ const res = await func( params ) as any
23
+ return res.data
24
+ }
25
+ }
@@ -0,0 +1,135 @@
1
+ (global as any).XMLHttpRequest = require('xhr2')
2
+ import { FirebaseCloudStorage } from './firebase-cloud-storage'
3
+ import { FirebaseHelper } from '../firebase-helper'
4
+ import { FirebaseDatasource } from '../store/firebase-datasource'
5
+ import { CloudStorage, Model, Persistent, persistent, registerPersistentClass, Store, StoredFile } from 'entropic-bond'
6
+
7
+ // Note about tests leaking. I've been checking and looks like firebase.storage
8
+ // methods are the responsible for the test leaking (as firebase v. 8.6.3).
9
+
10
+ class File {
11
+ data: Uint8Array | undefined
12
+ name: string | undefined
13
+ lastModified: any
14
+ size: number | undefined
15
+ type: any
16
+ arrayBuffer: any
17
+ slice: any
18
+ stream: any
19
+ text: any
20
+ }
21
+ global['File'] = File as any
22
+
23
+ @registerPersistentClass( 'Test' )
24
+ class Test extends Persistent {
25
+
26
+ get file(): StoredFile {
27
+ return this._file
28
+ }
29
+
30
+ @persistent private _file: StoredFile = new StoredFile()
31
+ }
32
+
33
+ describe( 'Firebase Cloud Storage', ()=>{
34
+ const blobData1 = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21]);
35
+ const blobData2 = new Uint8Array([0x6c, 0x6c, 0x6f, 0x2c, 0x48, 0x65, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21]);
36
+ let file: StoredFile
37
+
38
+ FirebaseHelper.setFirebaseConfig({
39
+ projectId: 'demo-test',
40
+ storageBucket: 'default-bucket'
41
+ })
42
+ FirebaseHelper.useEmulator({ firestorePort: 9080 })
43
+ Store.useDataSource( new FirebaseDatasource() )
44
+
45
+ beforeEach(()=>{
46
+ CloudStorage.useCloudStorage( new FirebaseCloudStorage() )
47
+ file = new StoredFile()
48
+ })
49
+
50
+ it( 'should save and get a url', async ()=>{
51
+ await file.save({ data:blobData1 })
52
+
53
+ expect( file.url ).toContain( file.id )
54
+ })
55
+
56
+ it( 'should report metadata', async ()=>{
57
+ await file.save({ data: blobData1, fileName: 'test.dat' })
58
+
59
+ expect( file.originalFileName ).toEqual( 'test.dat' )
60
+ expect( file.provider.className ).toEqual( 'FirebaseCloudStorage' )
61
+ })
62
+
63
+ it( 'should delete file', async ()=>{
64
+ await file.save({ data:blobData1 })
65
+
66
+ await file.delete()
67
+ expect( file.url ).not.toBeDefined()
68
+ })
69
+
70
+ it( 'should overwrite file on subsequent writes', async ()=>{
71
+ await file.save({ data:blobData1 })
72
+ const firstUrl = file.url
73
+ let resp = await fetch( file.url! )
74
+ expect( await resp.text() ).toEqual( 'Hello, world!')
75
+
76
+ await file.save({ data:blobData2 })
77
+ resp = await fetch( file.url! )
78
+ expect(
79
+ file.url?.slice( 0, file.url.indexOf('token') )
80
+ ).toEqual(
81
+ firstUrl?.slice( 0, firstUrl.indexOf('token') )
82
+ )
83
+ expect( await resp.text() ).toEqual( 'llo,He world!')
84
+ })
85
+
86
+ it( 'should trigger events', async () => {
87
+ const cb = vi.fn()
88
+
89
+ const savePromise = file.save({ data:blobData1 })
90
+ file.uploadControl().onProgress( cb )
91
+ await savePromise
92
+
93
+ expect( cb ).toHaveBeenCalledTimes( 2 )
94
+ })
95
+
96
+ describe( 'Streaming', ()=>{
97
+ let model: Model<Test>
98
+ let testObj: Test
99
+
100
+ beforeEach(()=>{
101
+ testObj = new Test()
102
+ model = Store.getModel<Test>( testObj )
103
+ })
104
+
105
+ it( 'should load object with StoredFile', async ()=>{
106
+ await testObj.file.save({ data: blobData1, fileName: 'test.dat' })
107
+ await model.save( testObj )
108
+
109
+ const newTestObj = await model.findById( testObj.id )
110
+
111
+ expect( newTestObj?.file ).toBeInstanceOf( StoredFile )
112
+ expect( newTestObj?.file.url ).toContain( testObj.file.id )
113
+ })
114
+
115
+ it( 'should replace file on save after load', async ()=>{
116
+ const deleteSpy = vi.spyOn( testObj.file, 'delete' )
117
+
118
+ await testObj.file.save({ data: blobData1, fileName: 'test.dat' })
119
+ await model.save( testObj )
120
+
121
+ const newTestObj = await model.findById( testObj.id )
122
+
123
+ expect( newTestObj?.file ).toBeInstanceOf( StoredFile )
124
+ expect( newTestObj?.file.url ).toContain( testObj.file.id )
125
+ expect( deleteSpy ).not.toHaveBeenCalled()
126
+
127
+ testObj.file.setDataToStore( blobData2 )
128
+ await testObj.file.save()
129
+
130
+ expect( deleteSpy ).toHaveBeenCalled()
131
+ })
132
+
133
+ })
134
+
135
+ })