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.
- package/.github/workflows/release.yml +26 -0
- package/CHANGELOG.md +1151 -0
- package/docs/.nojekyll +1 -0
- package/docs/README.md +94 -0
- package/docs/classes/Auth.md +391 -0
- package/docs/classes/AuthMock.md +278 -0
- package/docs/classes/AuthService.md +188 -0
- package/docs/classes/CloudFunctions.md +123 -0
- package/docs/classes/CloudFunctionsMock.md +97 -0
- package/docs/classes/CloudStorage.md +215 -0
- package/docs/classes/DataSource.md +248 -0
- package/docs/classes/EntropicComponent.md +666 -0
- package/docs/classes/JsonDataSource.md +328 -0
- package/docs/classes/MockCloudStorage.md +279 -0
- package/docs/classes/Model.md +274 -0
- package/docs/classes/Observable.md +120 -0
- package/docs/classes/Persistent.md +420 -0
- package/docs/classes/ServerAuth.md +211 -0
- package/docs/classes/ServerAuthMock.md +176 -0
- package/docs/classes/ServerAuthService.md +130 -0
- package/docs/classes/Store.md +218 -0
- package/docs/classes/StoredFile.md +636 -0
- package/docs/enums/StoredFileEvent.md +41 -0
- package/docs/interfaces/AuthError.md +30 -0
- package/docs/interfaces/CloudFunctionsService.md +69 -0
- package/docs/interfaces/Collection.md +13 -0
- package/docs/interfaces/CustomCredentials.md +7 -0
- package/docs/interfaces/DocumentReference.md +49 -0
- package/docs/interfaces/JsonRawData.md +7 -0
- package/docs/interfaces/SignData.md +63 -0
- package/docs/interfaces/StoreParams.md +52 -0
- package/docs/interfaces/StoredFileChange.md +41 -0
- package/docs/interfaces/UploadControl.md +90 -0
- package/docs/interfaces/UserCredentials.md +113 -0
- package/docs/interfaces/Values.md +17 -0
- package/docs/modules.md +1273 -0
- package/package.json +23 -19
- package/src/auth/auth-mock.spec.ts +168 -0
- package/src/auth/auth-mock.ts +129 -0
- package/src/auth/auth.ts +185 -0
- package/src/auth/user-auth-types.ts +21 -0
- package/src/cloud-functions/cloud-functions-mock.spec.ts +136 -0
- package/src/cloud-functions/cloud-functions-mock.ts +23 -0
- package/src/cloud-functions/cloud-functions.ts +83 -0
- package/src/cloud-storage/cloud-storage.spec.ts +207 -0
- package/src/cloud-storage/cloud-storage.ts +60 -0
- package/src/cloud-storage/mock-cloud-storage.ts +72 -0
- package/src/cloud-storage/stored-file.ts +102 -0
- package/src/index.ts +19 -0
- package/src/observable/observable.spec.ts +105 -0
- package/src/observable/observable.ts +67 -0
- package/src/persistent/entropic-component.spec.ts +143 -0
- package/src/persistent/entropic-component.ts +135 -0
- package/src/persistent/persistent.spec.ts +828 -0
- package/src/persistent/persistent.ts +650 -0
- package/src/server-auth/server-auth-mock.spec.ts +53 -0
- package/src/server-auth/server-auth-mock.ts +45 -0
- package/src/server-auth/server-auth.ts +49 -0
- package/src/store/data-source.ts +186 -0
- package/src/store/json-data-source.spec.ts +100 -0
- package/src/store/json-data-source.ts +256 -0
- package/src/store/mocks/mock-data.json +155 -0
- package/src/store/mocks/test-user.ts +122 -0
- package/src/store/model.spec.ts +659 -0
- package/src/store/model.ts +462 -0
- package/src/store/store.spec.ts +30 -0
- package/src/store/store.ts +113 -0
- package/src/types/utility-types.spec.ts +117 -0
- package/src/types/utility-types.ts +116 -0
- package/src/utils/test-utils/test-person.ts +44 -0
- package/src/utils/utils.spec.ts +95 -0
- package/{lib/utils/utils.d.ts → src/utils/utils.ts} +34 -10
- package/tsconfig-build.json +7 -0
- package/tsconfig-cjs.json +9 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +22 -0
- package/lib/auth/auth-mock.d.ts +0 -21
- package/lib/auth/auth-mock.js +0 -108
- package/lib/auth/auth-mock.js.map +0 -1
- package/lib/auth/auth.d.ts +0 -129
- package/lib/auth/auth.js +0 -146
- package/lib/auth/auth.js.map +0 -1
- package/lib/auth/user-auth-types.d.ts +0 -19
- package/lib/auth/user-auth-types.js +0 -3
- package/lib/auth/user-auth-types.js.map +0 -1
- package/lib/cloud-functions/cloud-functions-mock.d.ts +0 -11
- package/lib/cloud-functions/cloud-functions-mock.js +0 -19
- package/lib/cloud-functions/cloud-functions-mock.js.map +0 -1
- package/lib/cloud-functions/cloud-functions.d.ts +0 -19
- package/lib/cloud-functions/cloud-functions.js +0 -64
- package/lib/cloud-functions/cloud-functions.js.map +0 -1
- package/lib/cloud-storage/cloud-storage.d.ts +0 -24
- package/lib/cloud-storage/cloud-storage.js +0 -37
- package/lib/cloud-storage/cloud-storage.js.map +0 -1
- package/lib/cloud-storage/mock-cloud-storage.d.ts +0 -20
- package/lib/cloud-storage/mock-cloud-storage.js +0 -68
- package/lib/cloud-storage/mock-cloud-storage.js.map +0 -1
- package/lib/cloud-storage/stored-file.d.ts +0 -39
- package/lib/cloud-storage/stored-file.js +0 -106
- package/lib/cloud-storage/stored-file.js.map +0 -1
- package/lib/index.d.ts +0 -19
- package/lib/index.js +0 -36
- package/lib/index.js.map +0 -1
- package/lib/observable/observable.d.ts +0 -52
- package/lib/observable/observable.js +0 -66
- package/lib/observable/observable.js.map +0 -1
- package/lib/persistent/entropic-component.d.ts +0 -76
- package/lib/persistent/entropic-component.js +0 -109
- package/lib/persistent/entropic-component.js.map +0 -1
- package/lib/persistent/persistent.d.ts +0 -281
- package/lib/persistent/persistent.js +0 -539
- package/lib/persistent/persistent.js.map +0 -1
- package/lib/server-auth/server-auth-mock.d.ts +0 -12
- package/lib/server-auth/server-auth-mock.js +0 -39
- package/lib/server-auth/server-auth-mock.js.map +0 -1
- package/lib/server-auth/server-auth.d.ts +0 -24
- package/lib/server-auth/server-auth.js +0 -36
- package/lib/server-auth/server-auth.js.map +0 -1
- package/lib/store/data-source.d.ts +0 -137
- package/lib/store/data-source.js +0 -62
- package/lib/store/data-source.js.map +0 -1
- package/lib/store/json-data-source.d.ts +0 -68
- package/lib/store/json-data-source.js +0 -199
- package/lib/store/json-data-source.js.map +0 -1
- package/lib/store/mocks/test-user.d.ts +0 -49
- package/lib/store/mocks/test-user.js +0 -135
- package/lib/store/mocks/test-user.js.map +0 -1
- package/lib/store/model.d.ts +0 -238
- package/lib/store/model.js +0 -417
- package/lib/store/model.js.map +0 -1
- package/lib/store/store.d.ts +0 -62
- package/lib/store/store.js +0 -102
- package/lib/store/store.js.map +0 -1
- package/lib/types/utility-types.d.ts +0 -45
- package/lib/types/utility-types.js +0 -3
- package/lib/types/utility-types.js.map +0 -1
- package/lib/utils/test-utils/test-person.d.ts +0 -33
- package/lib/utils/test-utils/test-person.js +0 -25
- package/lib/utils/test-utils/test-person.js.map +0 -1
- package/lib/utils/utils.js +0 -76
- 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
|
+
}
|