entropic-bond 1.48.1 → 1.50.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 (141) hide show
  1. package/.github/workflows/release.yml +26 -0
  2. package/CHANGELOG.md +1151 -0
  3. package/docs/.nojekyll +1 -0
  4. package/docs/README.md +94 -0
  5. package/docs/classes/Auth.md +391 -0
  6. package/docs/classes/AuthMock.md +278 -0
  7. package/docs/classes/AuthService.md +188 -0
  8. package/docs/classes/CloudFunctions.md +123 -0
  9. package/docs/classes/CloudFunctionsMock.md +97 -0
  10. package/docs/classes/CloudStorage.md +215 -0
  11. package/docs/classes/DataSource.md +248 -0
  12. package/docs/classes/EntropicComponent.md +666 -0
  13. package/docs/classes/JsonDataSource.md +328 -0
  14. package/docs/classes/MockCloudStorage.md +279 -0
  15. package/docs/classes/Model.md +274 -0
  16. package/docs/classes/Observable.md +120 -0
  17. package/docs/classes/Persistent.md +420 -0
  18. package/docs/classes/ServerAuth.md +211 -0
  19. package/docs/classes/ServerAuthMock.md +176 -0
  20. package/docs/classes/ServerAuthService.md +130 -0
  21. package/docs/classes/Store.md +218 -0
  22. package/docs/classes/StoredFile.md +636 -0
  23. package/docs/enums/StoredFileEvent.md +41 -0
  24. package/docs/interfaces/AuthError.md +30 -0
  25. package/docs/interfaces/CloudFunctionsService.md +69 -0
  26. package/docs/interfaces/Collection.md +13 -0
  27. package/docs/interfaces/CustomCredentials.md +7 -0
  28. package/docs/interfaces/DocumentReference.md +49 -0
  29. package/docs/interfaces/JsonRawData.md +7 -0
  30. package/docs/interfaces/SignData.md +63 -0
  31. package/docs/interfaces/StoreParams.md +52 -0
  32. package/docs/interfaces/StoredFileChange.md +41 -0
  33. package/docs/interfaces/UploadControl.md +90 -0
  34. package/docs/interfaces/UserCredentials.md +113 -0
  35. package/docs/interfaces/Values.md +17 -0
  36. package/docs/modules.md +1273 -0
  37. package/package.json +23 -19
  38. package/src/auth/auth-mock.spec.ts +168 -0
  39. package/src/auth/auth-mock.ts +129 -0
  40. package/src/auth/auth.ts +185 -0
  41. package/src/auth/user-auth-types.ts +21 -0
  42. package/src/cloud-functions/cloud-functions-mock.spec.ts +136 -0
  43. package/src/cloud-functions/cloud-functions-mock.ts +23 -0
  44. package/src/cloud-functions/cloud-functions.ts +83 -0
  45. package/src/cloud-storage/cloud-storage.spec.ts +207 -0
  46. package/src/cloud-storage/cloud-storage.ts +60 -0
  47. package/src/cloud-storage/mock-cloud-storage.ts +72 -0
  48. package/src/cloud-storage/stored-file.ts +102 -0
  49. package/src/index.ts +19 -0
  50. package/src/observable/observable.spec.ts +105 -0
  51. package/src/observable/observable.ts +67 -0
  52. package/src/persistent/entropic-component.spec.ts +143 -0
  53. package/src/persistent/entropic-component.ts +135 -0
  54. package/src/persistent/persistent.spec.ts +828 -0
  55. package/src/persistent/persistent.ts +650 -0
  56. package/src/server-auth/server-auth-mock.spec.ts +53 -0
  57. package/src/server-auth/server-auth-mock.ts +45 -0
  58. package/src/server-auth/server-auth.ts +49 -0
  59. package/src/store/data-source.ts +186 -0
  60. package/src/store/json-data-source.spec.ts +100 -0
  61. package/src/store/json-data-source.ts +256 -0
  62. package/src/store/mocks/mock-data.json +155 -0
  63. package/src/store/mocks/test-user.ts +122 -0
  64. package/src/store/model.spec.ts +659 -0
  65. package/src/store/model.ts +462 -0
  66. package/src/store/store.spec.ts +30 -0
  67. package/src/store/store.ts +113 -0
  68. package/src/types/utility-types.spec.ts +117 -0
  69. package/src/types/utility-types.ts +116 -0
  70. package/src/utils/test-utils/test-person.ts +44 -0
  71. package/src/utils/utils.spec.ts +95 -0
  72. package/{lib/utils/utils.d.ts → src/utils/utils.ts} +34 -10
  73. package/tsconfig-build.json +7 -0
  74. package/tsconfig-cjs.json +9 -0
  75. package/tsconfig.json +33 -0
  76. package/vite.config.ts +22 -0
  77. package/lib/auth/auth-mock.d.ts +0 -21
  78. package/lib/auth/auth-mock.js +0 -108
  79. package/lib/auth/auth-mock.js.map +0 -1
  80. package/lib/auth/auth.d.ts +0 -129
  81. package/lib/auth/auth.js +0 -146
  82. package/lib/auth/auth.js.map +0 -1
  83. package/lib/auth/user-auth-types.d.ts +0 -19
  84. package/lib/auth/user-auth-types.js +0 -3
  85. package/lib/auth/user-auth-types.js.map +0 -1
  86. package/lib/cloud-functions/cloud-functions-mock.d.ts +0 -11
  87. package/lib/cloud-functions/cloud-functions-mock.js +0 -19
  88. package/lib/cloud-functions/cloud-functions-mock.js.map +0 -1
  89. package/lib/cloud-functions/cloud-functions.d.ts +0 -19
  90. package/lib/cloud-functions/cloud-functions.js +0 -64
  91. package/lib/cloud-functions/cloud-functions.js.map +0 -1
  92. package/lib/cloud-storage/cloud-storage.d.ts +0 -24
  93. package/lib/cloud-storage/cloud-storage.js +0 -37
  94. package/lib/cloud-storage/cloud-storage.js.map +0 -1
  95. package/lib/cloud-storage/mock-cloud-storage.d.ts +0 -20
  96. package/lib/cloud-storage/mock-cloud-storage.js +0 -68
  97. package/lib/cloud-storage/mock-cloud-storage.js.map +0 -1
  98. package/lib/cloud-storage/stored-file.d.ts +0 -39
  99. package/lib/cloud-storage/stored-file.js +0 -106
  100. package/lib/cloud-storage/stored-file.js.map +0 -1
  101. package/lib/index.d.ts +0 -19
  102. package/lib/index.js +0 -36
  103. package/lib/index.js.map +0 -1
  104. package/lib/observable/observable.d.ts +0 -52
  105. package/lib/observable/observable.js +0 -66
  106. package/lib/observable/observable.js.map +0 -1
  107. package/lib/persistent/entropic-component.d.ts +0 -76
  108. package/lib/persistent/entropic-component.js +0 -109
  109. package/lib/persistent/entropic-component.js.map +0 -1
  110. package/lib/persistent/persistent.d.ts +0 -281
  111. package/lib/persistent/persistent.js +0 -539
  112. package/lib/persistent/persistent.js.map +0 -1
  113. package/lib/server-auth/server-auth-mock.d.ts +0 -12
  114. package/lib/server-auth/server-auth-mock.js +0 -39
  115. package/lib/server-auth/server-auth-mock.js.map +0 -1
  116. package/lib/server-auth/server-auth.d.ts +0 -24
  117. package/lib/server-auth/server-auth.js +0 -36
  118. package/lib/server-auth/server-auth.js.map +0 -1
  119. package/lib/store/data-source.d.ts +0 -137
  120. package/lib/store/data-source.js +0 -62
  121. package/lib/store/data-source.js.map +0 -1
  122. package/lib/store/json-data-source.d.ts +0 -68
  123. package/lib/store/json-data-source.js +0 -199
  124. package/lib/store/json-data-source.js.map +0 -1
  125. package/lib/store/mocks/test-user.d.ts +0 -49
  126. package/lib/store/mocks/test-user.js +0 -135
  127. package/lib/store/mocks/test-user.js.map +0 -1
  128. package/lib/store/model.d.ts +0 -238
  129. package/lib/store/model.js +0 -417
  130. package/lib/store/model.js.map +0 -1
  131. package/lib/store/store.d.ts +0 -62
  132. package/lib/store/store.js +0 -102
  133. package/lib/store/store.js.map +0 -1
  134. package/lib/types/utility-types.d.ts +0 -45
  135. package/lib/types/utility-types.js +0 -3
  136. package/lib/types/utility-types.js.map +0 -1
  137. package/lib/utils/test-utils/test-person.d.ts +0 -33
  138. package/lib/utils/test-utils/test-person.js +0 -25
  139. package/lib/utils/test-utils/test-person.js.map +0 -1
  140. package/lib/utils/utils.js +0 -76
  141. package/lib/utils/utils.js.map +0 -1
@@ -0,0 +1,136 @@
1
+ import { PersistentObject } from '../persistent/persistent'
2
+ import { TestUser } from '../store/mocks/test-user'
3
+ import { CloudFunctions } from './cloud-functions'
4
+ import { CloudFunctionsMock } from './cloud-functions-mock'
5
+
6
+ describe( 'Cloud functions', ()=>{
7
+
8
+ beforeEach(()=>{
9
+ CloudFunctions.useCloudFunctionsService( new CloudFunctionsMock({
10
+ testNoParam: ():Promise<string> => Promise.resolve( 'test' ),
11
+ testArbitraryParamAndReturn: ( data: any ): Promise<any> => Promise.resolve( data.length ),
12
+ testPersistentParamAndReturn: ( data: PersistentObject<TestUser> ): Promise<PersistentObject<TestUser>> => {
13
+ return Promise.resolve( JSON.parse( JSON.stringify( data ) ) )
14
+ },
15
+ testPersistentParamAndPlainReturn: ( data: PersistentObject<TestUser> ): Promise<number> => {
16
+ return Promise.resolve( data.age! )
17
+ },
18
+ testPlainParamAndPersistentReturn: ( data: number ): Promise<PersistentObject<TestUser>> => {
19
+ const user = new TestUser()
20
+ user.age = data
21
+ return Promise.resolve( user.toObject() )
22
+ },
23
+ testWithoutParam: (): Promise<string> => Promise.resolve( 'Hello from the other side' ),
24
+ testWithoutReturn: ( _data: string ): Promise<void> => Promise.resolve(),
25
+ testArrayParam: ( data: PersistentObject<TestUser>[] ) => Promise.resolve( data.map( d => d.id ) ),
26
+ testObjectParam: ( data: {[key:string]:PersistentObject<TestUser>} ) => Promise.resolve(
27
+ Object.entries( data ).reduce(( obj, [k,v] ) => {
28
+ obj[k] = v.id
29
+ return obj
30
+ }, {})
31
+ ),
32
+ testArrayResult: () => Promise.resolve([ new TestUser('userA').toObject(), new TestUser('userB').toObject() ]),
33
+ testObjectResult: () => Promise.resolve({ user1: new TestUser('userA').toObject(), user2: new TestUser('userB').toObject() })
34
+ }))
35
+
36
+ })
37
+
38
+ it( 'should execute cloud function without params', async ()=>{
39
+ const testNoParam = CloudFunctions.instance.getRawFunction<never, string>( 'testNoParam' )
40
+ const result = await testNoParam()
41
+ expect( result ).toEqual( 'test' )
42
+ })
43
+
44
+ it( 'should execute cloud function with params', async ()=>{
45
+ const testArbitraryParamAndReturn = CloudFunctions.instance.getFunction<string, number>( 'testArbitraryParamAndReturn' )
46
+
47
+ const result = await testArbitraryParamAndReturn( 'Hello' )
48
+ expect( result ).toEqual( 5 )
49
+ })
50
+
51
+ it( 'should execute cloud function without params', async ()=>{
52
+ const testWithoutReturn = CloudFunctions.instance.getFunction<string>( 'testWithoutReturn' )
53
+
54
+ expect( testWithoutReturn( '' ) ).resolves.toBeUndefined()
55
+ })
56
+
57
+ it( 'should execute cloud function with void return', async ()=>{
58
+ const testWithoutParam = CloudFunctions.instance.getFunction<string>( 'testWithoutParam' )
59
+
60
+ const result = await testWithoutParam()
61
+ expect( result ).toEqual( 'Hello from the other side' )
62
+ })
63
+
64
+ it( 'should execute cloud function with params and return as Persistent', async ()=>{
65
+ const testPersistentParamAndReturn = CloudFunctions.instance.getFunction<TestUser, TestUser>( 'testPersistentParamAndReturn' )
66
+
67
+ const user = new TestUser()
68
+ user.age = 35
69
+ user.name = { firstName: 'Test User', lastName: 'as a user', ancestorName: {} }
70
+
71
+ const result = await testPersistentParamAndReturn( user )
72
+ expect( result.age ).toBe( 35 )
73
+ expect( result.name ).toEqual({ firstName: 'Test User', lastName: 'as a user', ancestorName: {} })
74
+ })
75
+
76
+ it( 'should execute cloud function with params as Persistent and return as plain', async ()=>{
77
+ const testPersistentParamAndPlainReturn = CloudFunctions.instance.getFunction<TestUser, number>( 'testPersistentParamAndPlainReturn' )
78
+
79
+ const user = new TestUser()
80
+ user.age = 35
81
+
82
+ const result = await testPersistentParamAndPlainReturn( user )
83
+ expect( result ).toBe( 35 )
84
+ })
85
+
86
+ it( 'should execute cloud function with params as plain and return as Persistent', async ()=>{
87
+ const testPlainParamAndPersistentReturn = CloudFunctions.instance.getFunction<number, TestUser>( 'testPlainParamAndPersistentReturn' )
88
+
89
+ const result = await testPlainParamAndPersistentReturn( 35 )
90
+ expect( result.age ).toEqual( 35 )
91
+ })
92
+
93
+ it( 'should execute cloud function with params as an array of Persistents', async ()=>{
94
+ const testArrayParam = CloudFunctions.instance.getFunction<TestUser[], string[]>( 'testArrayParam' )
95
+
96
+ const result = await testArrayParam([
97
+ new TestUser('userA'),
98
+ new TestUser('userB')
99
+ ])
100
+
101
+ expect( result ).toEqual([ 'userA', 'userB' ])
102
+ })
103
+
104
+ it( 'should execute cloud functions that return array of Persistent', async ()=>{
105
+ const testArrayResult = CloudFunctions.instance.getFunction<void, TestUser[]>( 'testArrayResult' )
106
+
107
+ const result = await testArrayResult()
108
+
109
+ expect( result[0] ).toBeInstanceOf( TestUser )
110
+ expect( result[0]?.id ).toEqual( 'userA' )
111
+ expect( result[1]?.id ).toEqual( 'userB' )
112
+ })
113
+
114
+ it( 'should execute cloud function with params as an object containing Persistents', async ()=>{
115
+ const testObjectParam = CloudFunctions.instance.getFunction<{[key: string]:TestUser}, {[key: string]:string}>( 'testObjectParam' )
116
+
117
+ const result = await testObjectParam({
118
+ user1: new TestUser('userA'),
119
+ user2: new TestUser('userB')
120
+ })
121
+
122
+ expect( result ).toEqual({ user1: 'userA', user2: 'userB' })
123
+ })
124
+
125
+ it( 'should execute cloud functions that return an object with Persistent', async ()=>{
126
+ const testObjectResult = CloudFunctions.instance.getFunction<void, {[key: string]:TestUser}>( 'testObjectResult' )
127
+
128
+ const result = await testObjectResult()
129
+
130
+ expect( result.user1 ).toBeInstanceOf( TestUser )
131
+ expect( result.user1?.id ).toEqual( 'userA' )
132
+ expect( result.user2?.id ).toEqual( 'userB' )
133
+ })
134
+
135
+
136
+ })
@@ -0,0 +1,23 @@
1
+ import { CloudFunction, CloudFunctionsService } from './cloud-functions'
2
+
3
+ interface FunctionCollection {
4
+ [key: string]: CloudFunction<any,any>
5
+ }
6
+
7
+ export class CloudFunctionsMock implements CloudFunctionsService {
8
+ constructor( registeredFunctions: FunctionCollection ) {
9
+ this._registeredFunctions = registeredFunctions
10
+ }
11
+
12
+ retrieveFunction<P, R>( cloudFunction: string ): CloudFunction<P,R> {
13
+ const func = this._registeredFunctions[ cloudFunction ]
14
+ if ( !func ) throw new Error( `Cloud function ${ cloudFunction } is not registered.` )
15
+ return func
16
+ }
17
+
18
+ callFunction<P, R>( func: CloudFunction<P, R>, params: P ): Promise<R> {
19
+ return func( params )
20
+ }
21
+
22
+ private _registeredFunctions: FunctionCollection
23
+ }
@@ -0,0 +1,83 @@
1
+ import { Persistent, PersistentObject } from '../persistent/persistent'
2
+
3
+
4
+ export type CloudFunction<P,R> = ( param?: P) => Promise<R>
5
+
6
+ export interface CloudFunctionsService {
7
+ retrieveFunction<P, R>( cloudFunction: string ): CloudFunction<P, R>
8
+ callFunction<P,R>( func: CloudFunction<P, R>, params: P ): Promise<R>
9
+ }
10
+
11
+ export class CloudFunctions {
12
+ private constructor() {}
13
+
14
+ static error = { shouldBeRegistered: 'You should register a cloud functions service with useCloudFunctionsService static method before using CloudFunctions.' }
15
+
16
+ static useCloudFunctionsService( cloudFunctionsService: CloudFunctionsService ) {
17
+ if ( this._cloudFunctionsService != cloudFunctionsService ) {
18
+ this._cloudFunctionsService = cloudFunctionsService
19
+ }
20
+ }
21
+
22
+ static get instance() {
23
+ if ( !this._cloudFunctionsService ) throw new Error( CloudFunctions.error.shouldBeRegistered )
24
+ return CloudFunctions._instance || ( CloudFunctions._instance = new CloudFunctions() )
25
+ }
26
+
27
+ getRawFunction<P, R>( cloudFunction: string ): CloudFunction<P,R> {
28
+ return CloudFunctions._cloudFunctionsService.retrieveFunction( cloudFunction )
29
+ }
30
+
31
+ getFunction<P, R=void>( cloudFunction: string ): CloudFunction<P,R> {
32
+ const callFunction = CloudFunctions._cloudFunctionsService.callFunction
33
+ const func = this.getRawFunction<P, R>( cloudFunction )
34
+
35
+ return async ( param: P ) => {
36
+ const result = await callFunction( func, this.processParam( param ) )
37
+ return this.processResult( result )
38
+ }
39
+ }
40
+
41
+ private processParam<P>( param: P ): P | PersistentObject<P & Persistent> {
42
+ if ( param === undefined || param === null ) return undefined!
43
+
44
+ if ( param instanceof Persistent ) return param.toObject()
45
+
46
+ if ( Array.isArray( param ) ) {
47
+ return param.map( p => this.processParam( p ) ) as unknown as P
48
+ }
49
+
50
+ if ( typeof param === 'object' ) {
51
+ return Object.entries( param ).reduce(( newParam, [ key, value ])=>{
52
+ newParam[ key ] = this.processParam( value )
53
+ return newParam
54
+ }, {}) as P
55
+ }
56
+
57
+ return param
58
+ }
59
+
60
+ private processResult<R>( value: R | PersistentObject<R & Persistent> ): R {
61
+ if ( value === undefined || value === null ) return undefined!
62
+
63
+ if ( ( value as PersistentObject<R & Persistent> ).__className ) {
64
+ return Persistent.createInstance( value as PersistentObject<R & Persistent> ) as R
65
+ }
66
+
67
+ if ( Array.isArray( value ) ) {
68
+ return value.map( elem => this.processResult( elem ) ) as unknown as R
69
+ }
70
+
71
+ if ( typeof value === 'object' ) {
72
+ return Object.entries( value ).reduce((newVal, [ key, val ]) => {
73
+ newVal[ key ] = this.processResult( val )
74
+ return newVal
75
+ }, {}) as R
76
+ }
77
+
78
+ return value as R
79
+ }
80
+
81
+ private static _cloudFunctionsService: CloudFunctionsService
82
+ private static _instance: CloudFunctions
83
+ }
@@ -0,0 +1,207 @@
1
+ import { Persistent, persistent, registerPersistentClass } from '../persistent/persistent'
2
+ import { JsonDataSource } from '../store/json-data-source'
3
+ import { Model } from '../store/model'
4
+ import { Store } from '../store/store'
5
+ import { CloudStorage } from './cloud-storage'
6
+ import { MockCloudStorage } from './mock-cloud-storage'
7
+ import { StoredFile, StoredFileEvent } from './stored-file'
8
+
9
+ function MockFile(this: any, data: any[], filename: string ) {
10
+ this.data = data as any[]
11
+ this.name = filename
12
+ }
13
+
14
+ global.File = MockFile as any
15
+
16
+ @registerPersistentClass( 'Test' )
17
+ class Test extends Persistent {
18
+
19
+ get file(): StoredFile {
20
+ return this._file
21
+ }
22
+
23
+ @persistent private _file: StoredFile = new StoredFile()
24
+ }
25
+
26
+ describe( 'Cloud Storage', ()=>{
27
+ let testObj: Test
28
+ let file: StoredFile
29
+ const blobData1 = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21]);
30
+ const blobData2 = new Uint8Array([0x6c, 0x6c, 0x6f, 0x2c, 0x48, 0x65, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21]);
31
+ const fileData = new MockFile( [blobData1], 'pepe.dat' )
32
+
33
+ const mockCloudStorage = new MockCloudStorage( 'mock-data-folder/' )
34
+
35
+ beforeEach(()=>{
36
+ CloudStorage.useCloudStorage( mockCloudStorage )
37
+ testObj = new Test()
38
+ file = new StoredFile()
39
+ })
40
+
41
+ afterEach( ()=> mockCloudStorage.mockFileSystem = {} )
42
+
43
+ it( 'should save a file from Blob', async ()=>{
44
+ await file.save({ data: blobData1 })
45
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toBeDefined()
46
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toEqual( JSON.stringify( blobData1 ) )
47
+ })
48
+
49
+ it( 'should save a file from File', async ()=>{
50
+ await file.save({ data: fileData })
51
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toBeDefined()
52
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toEqual( JSON.stringify( {data:[blobData1], name: 'pepe.dat'} ) )
53
+ expect( file.originalFileName ).toEqual( 'pepe.dat' )
54
+ expect( file.url ).toEqual( 'mock-data-folder/pepe.dat' )
55
+ })
56
+
57
+ it( 'should get a url', async ()=>{
58
+ await file.save({ data: blobData1 })
59
+
60
+ expect( file.url ).toEqual( 'mock-data-folder/' + file.id )
61
+ })
62
+
63
+ it( 'should report metadata', async ()=>{
64
+ await file.save({ data: blobData1, fileName: 'test.dat' })
65
+
66
+ expect( file.originalFileName ).toEqual( 'test.dat' )
67
+ expect( file.provider.className ).toEqual( 'MockCloudStorage' )
68
+ })
69
+
70
+ it( 'should delete file', async ()=>{
71
+ await file.save({ data: blobData1 })
72
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toBeDefined()
73
+
74
+ await file.delete()
75
+ expect( file.url ).toBeUndefined()
76
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).not.toBeDefined()
77
+ })
78
+
79
+ it( 'should throw if not stored file', async ()=>{
80
+ let thrown = false
81
+
82
+ try{
83
+ await file.delete()
84
+ }
85
+ catch {
86
+ thrown = true
87
+ }
88
+
89
+ expect( thrown ).toBeTruthy()
90
+ })
91
+
92
+
93
+ it( 'should overwrite file on subsequent writes', async ()=>{
94
+ const deleteSpy = vi.spyOn( file, 'delete' )
95
+
96
+ await file.save({ data: 'first write' as any })
97
+ expect( deleteSpy ).not.toHaveBeenCalled()
98
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toEqual( '"first write"' )
99
+
100
+ await file.save({ data: 'second write' as any })
101
+ expect( deleteSpy ).toHaveBeenCalled()
102
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toEqual( '"second write"' )
103
+ })
104
+
105
+ it( 'should save from pending file', async ()=>{
106
+ file.setDataToStore( fileData )
107
+ await file.save()
108
+
109
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toBeDefined()
110
+ expect( mockCloudStorage.mockFileSystem[ file.id ] ).toEqual( JSON.stringify( {data:[blobData1], name: 'pepe.dat'} ) )
111
+ })
112
+
113
+ describe( 'Notify on change', ()=>{
114
+ let spy: vi.Mock
115
+
116
+ beforeEach(()=>{
117
+ spy = vi.fn()
118
+ file.onChange( spy )
119
+ })
120
+
121
+ afterEach( ()=> spy.mockClear() )
122
+
123
+ it( 'should notify on seting pendind data to store', ()=>{
124
+ file.setDataToStore( fileData )
125
+ expect( spy ).toHaveBeenNthCalledWith( 1, {
126
+ event: StoredFileEvent.pendingDataSet,
127
+ pendingData: fileData,
128
+ storedFile: file
129
+ })
130
+ })
131
+
132
+ it( 'should notify on data store', async ()=>{
133
+ await file.save( fileData )
134
+ expect( spy ).toHaveBeenNthCalledWith( 1, {
135
+ event: StoredFileEvent.stored,
136
+ storedFile: file
137
+ })
138
+ })
139
+
140
+ it( 'should notify on delete', async ()=>{
141
+ await file.save( fileData )
142
+ spy.mockClear()
143
+
144
+ await file.delete()
145
+ expect( spy ).toHaveBeenNthCalledWith( 1, {
146
+ event: StoredFileEvent.deleted,
147
+ storedFile: file
148
+ })
149
+ })
150
+ })
151
+
152
+ describe( 'Streaming', ()=>{
153
+ const database = {}
154
+ let model: Model<Test>
155
+ let testObj: Test
156
+
157
+ beforeEach(()=>{
158
+ Store.useDataSource( new JsonDataSource( database ) )
159
+ testObj = new Test()
160
+ model = Store.getModel<Test>( testObj )
161
+ })
162
+
163
+ it( 'should save object with StoredFile', async ()=>{
164
+ await testObj.file.save({ data: blobData1, fileName: 'test.dat' })
165
+ await model.save( testObj )
166
+
167
+ expect( database[ testObj.className ][ testObj.id ].file ).toBeDefined()
168
+ expect( database[ testObj.className ][ testObj.id ].file.reference ).toEqual( testObj.file.id )
169
+ expect( database[ testObj.className ][ testObj.id ].file.url ).toEqual( 'mock-data-folder/' + testObj.file.id )
170
+ expect( database[ testObj.className ][ testObj.id ].file.cloudStorageProviderName ).toEqual( 'MockCloudStorage' )
171
+ expect( database[ testObj.className ][ testObj.id ].file.originalFileName ).toEqual( 'test.dat' )
172
+ })
173
+
174
+ it( 'should load object with StoredFile', async ()=>{
175
+ await testObj.file.save({ data: blobData1, fileName: 'test.dat' })
176
+ await model.save( testObj )
177
+
178
+ const newTestObj = await model.findById( testObj.id )
179
+
180
+ expect( newTestObj?.file ).toBeInstanceOf( StoredFile )
181
+ expect( newTestObj?.file.url ).toEqual( 'mock-data-folder/' + testObj.file.id )
182
+ })
183
+
184
+ it( 'should replace file on save after load', async ()=>{
185
+ const deleteSpy = vi.spyOn( testObj.file, 'delete' )
186
+
187
+ await testObj.file.save({ data: blobData1, fileName: 'test.dat' })
188
+ await model.save( testObj )
189
+
190
+ const newTestObj = await model.findById( testObj.id )
191
+
192
+ expect( newTestObj?.file ).toBeInstanceOf( StoredFile )
193
+ expect( newTestObj?.file.url ).toEqual( 'mock-data-folder/' + testObj.file.id )
194
+ expect( deleteSpy ).not.toHaveBeenCalled()
195
+
196
+ testObj.file.setDataToStore( blobData2 )
197
+ await testObj.file.save()
198
+
199
+ expect(
200
+ mockCloudStorage.mockFileSystem[ testObj.file.id ]
201
+ ).toEqual( JSON.stringify( blobData2 ) )
202
+
203
+ expect( deleteSpy ).toHaveBeenCalled()
204
+ })
205
+
206
+ })
207
+ })
@@ -0,0 +1,60 @@
1
+ export type UploadProgress = ( uploadedBytes: number, fileSize: number ) => void
2
+
3
+ type CloudStorageFactory = ()=>CloudStorage
4
+
5
+ interface CloudStorageFactoryMap {
6
+ [ cloudStorageProviderName: string ] : CloudStorageFactory
7
+ }
8
+
9
+ export interface UploadControl {
10
+ pause: ()=>void
11
+ resume: ()=>void
12
+ cancel: ()=>void
13
+ onProgress: ( callback: UploadProgress )=>void
14
+ }
15
+
16
+ export type StorableData = File | Blob | Uint8Array | ArrayBuffer
17
+
18
+ export abstract class CloudStorage {
19
+ abstract save( id: string, data: StorableData, progress?: UploadProgress ): Promise<string>
20
+ abstract getUrl( reference: string ): Promise<string>
21
+ abstract uploadControl(): UploadControl
22
+ abstract delete( reference: string ): Promise<void>
23
+
24
+ static registerCloudStorage( cloudStorageProviderName: string, factory: CloudStorageFactory ) {
25
+ CloudStorage._cloudStorageFactoryMap[ cloudStorageProviderName ] = factory
26
+ }
27
+
28
+ static createInstance( providerName: string ) {
29
+ const provider = CloudStorage._cloudStorageFactoryMap[ providerName ]
30
+ if ( !provider ) {
31
+ throw new Error( `You should register the ${ providerName } cloud storage provider prior to use it`)
32
+ }
33
+
34
+ return provider()
35
+ }
36
+
37
+ get className(): string {
38
+ return this[ '__className' ];
39
+ }
40
+
41
+ static useCloudStorage( provider: CloudStorage ) {
42
+ CloudStorage._defaultCloudStorage = provider
43
+ }
44
+
45
+ static get defaultCloudStorage() {
46
+ if ( !CloudStorage._defaultCloudStorage ) {
47
+ throw new Error( 'You should define a default cloud storage provider prior to use it')
48
+ }
49
+ return CloudStorage._defaultCloudStorage
50
+ }
51
+ static _defaultCloudStorage: CloudStorage
52
+ private static _cloudStorageFactoryMap: CloudStorageFactoryMap = {}
53
+ }
54
+
55
+ export function registerCloudStorage( cloudStorageProviderName: string, factory: CloudStorageFactory ) {
56
+ CloudStorage.registerCloudStorage( cloudStorageProviderName, factory )
57
+ return ( constructor: Function ) => {
58
+ constructor.prototype.__className = cloudStorageProviderName
59
+ }
60
+ }
@@ -0,0 +1,72 @@
1
+ import { CloudStorage, registerCloudStorage, StorableData, UploadControl, UploadProgress } from './cloud-storage'
2
+
3
+ @registerCloudStorage( 'MockCloudStorage', ()=>new MockCloudStorage() )
4
+ export class MockCloudStorage extends CloudStorage {
5
+ constructor( pathToMockFiles: string = '' ) {
6
+ super()
7
+ this._pathToMockFiles = pathToMockFiles
8
+ }
9
+
10
+ /**
11
+ * Introduce a delay in the execution of operations to simulate a real data source
12
+ * @param miliSeconds the number of milliseconds to delay the execution of operations
13
+ * @returns a chainable reference to this object
14
+ */
15
+ simulateDelay( miliSeconds: number ) {
16
+ this._simulateDelay = miliSeconds
17
+ return this
18
+ }
19
+
20
+ private resolveWithDelay<T>( data?: T ): Promise<T> {
21
+ if ( this._simulateDelay <= 0 ) return Promise.resolve( data! )
22
+
23
+ const promise = new Promise<T>( resolve => {
24
+ setTimeout(
25
+ ()=> resolve( data! ),
26
+ this._simulateDelay
27
+ )
28
+ })
29
+ this._pendingPromises.push( promise )
30
+ promise.finally(
31
+ ()=> this._pendingPromises = this._pendingPromises.filter( p => p === promise )
32
+ )
33
+ return promise
34
+ }
35
+
36
+ save( id: string, data: StorableData ): Promise<string> {
37
+ const fullPath = id
38
+
39
+ if ( this._onProgress ) this._onProgress( 0, 100 )
40
+
41
+ this.mockFileSystem[ id ] = JSON.stringify( data )
42
+
43
+ if ( this._onProgress ) this._onProgress( 100, 100 )
44
+
45
+ const ref = data instanceof File? data.name : fullPath
46
+ return this.resolveWithDelay( ref )
47
+ }
48
+
49
+ uploadControl(): UploadControl {
50
+ return {
51
+ resume: ()=>{},
52
+ pause: ()=>{},
53
+ cancel: ()=>{},
54
+ onProgress: callback => this._onProgress = callback
55
+ }
56
+ }
57
+
58
+ getUrl( reference: string ): Promise<string> {
59
+ return Promise.resolve( this._pathToMockFiles + reference )
60
+ }
61
+
62
+ delete( reference: string ) {
63
+ delete this.mockFileSystem[ reference ]
64
+ return this.resolveWithDelay<void>()
65
+ }
66
+
67
+ private _simulateDelay: number = 0
68
+ private _pendingPromises: Promise<any>[] = []
69
+ private _onProgress: UploadProgress | undefined
70
+ private _pathToMockFiles: string
71
+ public mockFileSystem = {}
72
+ }
@@ -0,0 +1,102 @@
1
+ import { Callback, Observable } from '../observable/observable'
2
+ import { persistent, Persistent, registerPersistentClass } from '../persistent/persistent'
3
+ import { CloudStorage, StorableData, UploadControl, UploadProgress } from './cloud-storage'
4
+
5
+ export enum StoredFileEvent { stored, pendingDataSet, deleted }
6
+ export interface StoredFileChange {
7
+ event: StoredFileEvent
8
+ pendingData?: StorableData
9
+ storedFile: StoredFile
10
+ }
11
+
12
+ export interface StoreParams {
13
+ data?: StorableData,
14
+ fileName?: string,
15
+ progress?: UploadProgress,
16
+ cloudStorageProvider?: CloudStorage
17
+ }
18
+
19
+ @registerPersistentClass( 'StoredFile' )
20
+ export class StoredFile extends Persistent{
21
+
22
+ async save({ data, fileName, progress, cloudStorageProvider }: StoreParams = {}): Promise<void> {
23
+ const dataToStore = data || this._pendingData
24
+ if ( !dataToStore ) return
25
+ if ( this._reference ) await this.delete()
26
+
27
+ this.provider = cloudStorageProvider || CloudStorage.defaultCloudStorage
28
+ this._originalFileName = fileName || ( dataToStore instanceof File? dataToStore.name : undefined )
29
+
30
+ this._reference = await this.provider.save( this.id, dataToStore, progress )
31
+ this._url = await this.provider.getUrl( this._reference )
32
+
33
+ this._pendingData = undefined
34
+ this._onChange.notify({ event: StoredFileEvent.stored, storedFile: this })
35
+ }
36
+
37
+ uploadControl(): UploadControl {
38
+ return this.provider.uploadControl()
39
+ }
40
+
41
+ async delete(): Promise<void> {
42
+ if ( !this._reference ) throw new Error( 'Cannot delete a not stored file' )
43
+ await this.provider.delete( this._reference )
44
+ this._reference = undefined
45
+ this._url = undefined
46
+ this._onChange.notify({ event: StoredFileEvent.deleted, storedFile: this })
47
+ }
48
+
49
+ set provider( value: CloudStorage ) {
50
+ this._provider = value
51
+ this._cloudStorageProviderName = value.className
52
+ }
53
+
54
+ get provider() {
55
+ if ( !this._provider ) {
56
+ try {
57
+ this._provider = CloudStorage.createInstance( this._cloudStorageProviderName! )
58
+ }
59
+ catch {
60
+ this._provider = CloudStorage.defaultCloudStorage
61
+ }
62
+ }
63
+ return this._provider
64
+ }
65
+
66
+ get url() {
67
+ return this._url
68
+ }
69
+
70
+ get mimeType() {
71
+ return this._mimeType
72
+ }
73
+
74
+ setDataToStore( data: StorableData ) {
75
+ this._pendingData = data
76
+ this._originalFileName = data instanceof File? data.name : undefined
77
+ this._mimeType = data instanceof Blob? data.type : undefined
78
+ this._onChange.notify({
79
+ event: StoredFileEvent.pendingDataSet,
80
+ pendingData: data,
81
+ storedFile: this
82
+ })
83
+ return this
84
+ }
85
+
86
+ get originalFileName() {
87
+ return this._originalFileName
88
+ }
89
+
90
+ onChange( listenerCallback: Callback<StoredFileChange> ) {
91
+ return this._onChange.subscribe( listenerCallback )
92
+ }
93
+
94
+ @persistent private _reference: string | undefined
95
+ @persistent private _url: string | undefined
96
+ @persistent private _cloudStorageProviderName: string | undefined
97
+ @persistent private _originalFileName: string | undefined
98
+ @persistent private _mimeType: string | undefined
99
+ private _provider: CloudStorage | undefined
100
+ private _pendingData: StorableData | undefined
101
+ private _onChange: Observable<StoredFileChange> = new Observable<StoredFileChange>()
102
+ }