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,45 @@
|
|
|
1
|
+
import { UserCredentials } from '../auth/user-auth-types'
|
|
2
|
+
import { Collection } from '../types/utility-types'
|
|
3
|
+
import { ServerAuthService, CustomCredentials } from './server-auth'
|
|
4
|
+
|
|
5
|
+
export class ServerAuthMock extends ServerAuthService {
|
|
6
|
+
constructor( userCredentials: Collection<UserCredentials<{}>> ) {
|
|
7
|
+
super()
|
|
8
|
+
this._userCredentials = userCredentials
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getUser<T extends CustomCredentials>( userId: string ): Promise<UserCredentials<T> | undefined> {
|
|
12
|
+
if ( !this._userCredentials[ userId ] ) Promise.resolve( undefined )
|
|
13
|
+
|
|
14
|
+
return Promise.resolve( this._userCredentials[ userId ] as UserCredentials<T> )
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setCustomCredentials<T extends CustomCredentials>( userId: string, customCredentials: T ): Promise<void> {
|
|
18
|
+
const userCredentials = this._userCredentials[ userId ]
|
|
19
|
+
if ( !userCredentials ) throw new Error( `User ${ userId } not found in the auth system` )
|
|
20
|
+
userCredentials.customData = { ...customCredentials }
|
|
21
|
+
|
|
22
|
+
return Promise.resolve()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
updateUser<T extends CustomCredentials>( userId: string, credentials: Partial<UserCredentials<T>> ): Promise<UserCredentials<T>> {
|
|
26
|
+
this._userCredentials[ userId ] = {
|
|
27
|
+
...this._userCredentials,
|
|
28
|
+
...credentials,
|
|
29
|
+
id: userId
|
|
30
|
+
} as UserCredentials<T>
|
|
31
|
+
|
|
32
|
+
return Promise.resolve( this._userCredentials[ userId ] as UserCredentials<T> )
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
deleteUser( userId: string ): Promise<void> {
|
|
36
|
+
delete this._userCredentials[ userId ]
|
|
37
|
+
return Promise.resolve()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get userCredentials() {
|
|
41
|
+
return this._userCredentials
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private _userCredentials: Collection<UserCredentials<{}>>
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { UserCredentials } from '../auth/user-auth-types'
|
|
2
|
+
|
|
3
|
+
export interface CustomCredentials {
|
|
4
|
+
[ key: string ]: unknown
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export abstract class ServerAuthService {
|
|
8
|
+
abstract setCustomCredentials<T extends CustomCredentials>( userId: string, customCredentials: T ): Promise<void>
|
|
9
|
+
abstract getUser<T extends CustomCredentials>( userId: string ): Promise<UserCredentials<T> | undefined>
|
|
10
|
+
abstract updateUser<T extends CustomCredentials>( userId: string, credentials: Partial<UserCredentials<T>> ): Promise<UserCredentials<T>>
|
|
11
|
+
abstract deleteUser( userId: string ): Promise<void>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ServerAuth extends ServerAuthService {
|
|
15
|
+
static error = { shouldBeRegistered: 'You should register a Server Auth service before using the Server Auth.' }
|
|
16
|
+
|
|
17
|
+
protected constructor(){ super() }
|
|
18
|
+
|
|
19
|
+
static useServerAuthService( authService: ServerAuthService ) {
|
|
20
|
+
if ( ServerAuth._authService != authService ) {
|
|
21
|
+
ServerAuth._authService = authService
|
|
22
|
+
this._instance = undefined
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static get instance() {
|
|
27
|
+
if ( !ServerAuth._authService ) throw new Error( ServerAuth.error.shouldBeRegistered )
|
|
28
|
+
return this._instance || (this._instance = new ServerAuth() )
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getUser<T extends CustomCredentials>( userId: string ): Promise<UserCredentials<T> | undefined> {
|
|
32
|
+
return ServerAuth._authService.getUser( userId )
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
updateUser<T extends CustomCredentials>( userId: string, credentials: Partial<UserCredentials<T>> ): Promise<UserCredentials<T>> {
|
|
36
|
+
return ServerAuth._authService.updateUser( userId, credentials )
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setCustomCredentials<T extends CustomCredentials>( userId: string, customCredentials: T ): Promise<void> {
|
|
40
|
+
return ServerAuth._authService.setCustomCredentials( userId, customCredentials )
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
deleteUser( userId: string ) {
|
|
44
|
+
return ServerAuth._authService.deleteUser( userId )
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private static _instance: ServerAuth | undefined = undefined
|
|
48
|
+
private static _authService: ServerAuthService
|
|
49
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Persistent, PersistentObject, Collections } from '../persistent/persistent'
|
|
2
|
+
import { ClassPropNames } from '../types/utility-types'
|
|
3
|
+
|
|
4
|
+
export type DocumentObject = PersistentObject<Persistent>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The query operators
|
|
8
|
+
* @param == equal
|
|
9
|
+
* @param != not equal
|
|
10
|
+
* @param < less than
|
|
11
|
+
* @param <= less than or equal
|
|
12
|
+
* @param > greater than
|
|
13
|
+
* @param >= greater than or equal
|
|
14
|
+
* @param contains array contains
|
|
15
|
+
* @param containsAny array contains any
|
|
16
|
+
* @param in in
|
|
17
|
+
* @param !in not in
|
|
18
|
+
*/
|
|
19
|
+
export type QueryOperator = '==' | '!=' | '<' | '<=' | '>' | '>=' | 'contains' | 'containsAny'// | 'in' | '!in'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A representation of a query operation
|
|
23
|
+
* @param property the name of the property to be used in the query
|
|
24
|
+
* @param operator the operator to be used in the query
|
|
25
|
+
* @param value the value to be used in the query
|
|
26
|
+
* @param aggregate if true, the query results will be aggregated using an `or` operator
|
|
27
|
+
*/
|
|
28
|
+
export type QueryOperation<T> = {
|
|
29
|
+
property: ClassPropNames<T>
|
|
30
|
+
operator: QueryOperator
|
|
31
|
+
value: Partial<T[ClassPropNames<T>]> | {[key:string]: unknown}
|
|
32
|
+
aggregate?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The sort order
|
|
37
|
+
* @param asc ascending order
|
|
38
|
+
* @param desc descending order
|
|
39
|
+
*/
|
|
40
|
+
export type QueryOrder = 'asc' | 'desc'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A representation of a full query
|
|
44
|
+
* @param operations the query operations to be performed
|
|
45
|
+
* @param limit the maximum number of items to be retrieved
|
|
46
|
+
* @param sort sort info
|
|
47
|
+
* @param sort.order the sort order
|
|
48
|
+
* @param sort.propertyName the name of the property to be used for sorting
|
|
49
|
+
*/
|
|
50
|
+
export type QueryObject<T> = {
|
|
51
|
+
operations?: QueryOperation<T>[]
|
|
52
|
+
limit?: number
|
|
53
|
+
sort?: {
|
|
54
|
+
order: QueryOrder
|
|
55
|
+
propertyName: ClassPropNames<T> | string
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The data source interface.
|
|
61
|
+
* It defines the methods that must be implemented by a data source
|
|
62
|
+
* A data source is able to retrieve and save data i.e: from a database, a file, a RestAPI, etc.
|
|
63
|
+
* You can derive from this class to implement your own data source with the
|
|
64
|
+
* A data source is used by the store to retrieve and save data.
|
|
65
|
+
*/
|
|
66
|
+
export abstract class DataSource {
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieves a document by id
|
|
70
|
+
* Implement the required logic to retrieve a document by id from your concrete
|
|
71
|
+
* the data source
|
|
72
|
+
* @param id the id of the document to be retrieved
|
|
73
|
+
* @param collectionName the name of the collection where the document is stored
|
|
74
|
+
* @returns a promise resolving to the document object. The document object is
|
|
75
|
+
* a plain object with the properties of the document class.
|
|
76
|
+
*/
|
|
77
|
+
abstract findById( id: string, collectionName: string ): Promise< DocumentObject >
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Retrieves all documents matching the query stored in the query object
|
|
81
|
+
* Implement the required logic to retrieve the documents that match the
|
|
82
|
+
* requirements in the query object from your concrete the data source
|
|
83
|
+
* @param queryObject the query object containing the query operations
|
|
84
|
+
* @param collectionName the name of the collection where the documents are stored
|
|
85
|
+
* @returns a promise resolving to an array of document objects. The document object is
|
|
86
|
+
* a plain object with the properties of the document class.
|
|
87
|
+
* @see QueryObject
|
|
88
|
+
* @see QueryOperation
|
|
89
|
+
* @see QueryOperator
|
|
90
|
+
* @see QueryOrder
|
|
91
|
+
* @see DocumentObject
|
|
92
|
+
*/
|
|
93
|
+
abstract find( queryObject: QueryObject<DocumentObject>, collectionName: string ): Promise< DocumentObject[] >
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Saves a document
|
|
97
|
+
* Implement the required logic to save the document in your concrete the data source
|
|
98
|
+
* @param object A collection of documents to be saved
|
|
99
|
+
* @returns a promise
|
|
100
|
+
*/
|
|
101
|
+
abstract save( object: Collections ): Promise< void >
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Deletes a document by id
|
|
105
|
+
* Implement the required logic to delete a document by id from your concrete
|
|
106
|
+
* data source
|
|
107
|
+
* @param id the id of the document to be deleted
|
|
108
|
+
* @param collectionName the name of the collection where the document is stored
|
|
109
|
+
* @returns a promise
|
|
110
|
+
*/
|
|
111
|
+
abstract delete( id: string, collectionName: string ): Promise<void>
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Retrieves the next bunch of documents matching the query stored in the query object
|
|
115
|
+
* Implement the required logic to retrieve the next bunch of documents that match the
|
|
116
|
+
* requirements in the query object from your concrete the data source
|
|
117
|
+
* @param limit the maximum number of items to be retrieved
|
|
118
|
+
* @returns a promise resolving to an array representing the next bunch of document objects
|
|
119
|
+
*/
|
|
120
|
+
abstract next( limit?: number ): Promise< DocumentObject[] >
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Retrieves the number of documents matching the query stored in the query object
|
|
124
|
+
* Implement the required logic to retrieve the number of documents that match the
|
|
125
|
+
* requirements in the query object from your concrete the data source
|
|
126
|
+
* @param queryObject the query object containing the query operations
|
|
127
|
+
* @param collectionName the name of the collection where the documents are stored
|
|
128
|
+
* @returns a promise resolving to the number of documents matching the query
|
|
129
|
+
* @see QueryObject
|
|
130
|
+
*/
|
|
131
|
+
abstract count( queryObject: QueryObject<DocumentObject>, collectionName: string ): Promise<number>
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Utility method to convert a query object to a property path query object
|
|
135
|
+
*
|
|
136
|
+
* @param queryObject the query object to be converted
|
|
137
|
+
* @returns a property path query object
|
|
138
|
+
* @example
|
|
139
|
+
* const queryObject = {
|
|
140
|
+
* operations: [{ property: 'name', operator: '==', value: { ancestorName: { father: 'Felipe' }}]
|
|
141
|
+
* }
|
|
142
|
+
* const propPathQueryObject = DataSource.toPropertyPathQueryObject( queryObject )
|
|
143
|
+
* // returned value: [{ property: 'name.ancestorName.father', operator: '==', value: 'Felipe' }]
|
|
144
|
+
*/
|
|
145
|
+
static toPropertyPathOperations<T extends Persistent>( operations: QueryOperation<T>[] ): QueryOperation<T>[] {
|
|
146
|
+
if ( !operations ) return []
|
|
147
|
+
|
|
148
|
+
return operations.map( operation => {
|
|
149
|
+
|
|
150
|
+
if ( DataSource.isArrayOperator( operation.operator ) && operation.value[0] instanceof Persistent ) {
|
|
151
|
+
return {
|
|
152
|
+
property: Persistent.searchableArrayNameFor( operation.property as string ),
|
|
153
|
+
operator: operation.operator,
|
|
154
|
+
value: ( operation.value as unknown as Persistent[] ).map( v => v.id ) as any,
|
|
155
|
+
aggregate: operation.aggregate
|
|
156
|
+
} as QueryOperation<T>
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const [ path, value ] = this.toPropertyPathValue( operation.value )
|
|
160
|
+
const propPath = `${ String( operation.property ) }${ path? '.'+path : '' }`
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
property: propPath,
|
|
164
|
+
operator: operation.operator,
|
|
165
|
+
value,
|
|
166
|
+
aggregate: operation.aggregate
|
|
167
|
+
} as QueryOperation<T>
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static isArrayOperator( operator: QueryOperator ): boolean {
|
|
172
|
+
return operator === 'containsAny' || operator === 'contains' //|| operator === 'in' || operator === '!in'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private static toPropertyPathValue( obj: {} ): [ string | undefined, unknown ] {
|
|
176
|
+
if ( typeof obj === 'object' && !Array.isArray( obj ) ) {
|
|
177
|
+
const propName = Object.keys( obj )[0]!
|
|
178
|
+
const [ propPath, value ] = this.toPropertyPathValue( obj[ propName ] )
|
|
179
|
+
return [ `${ propName }${ propPath? '.'+propPath : '' }`, value ]
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
return [ undefined, obj ]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DocumentObject, JsonDataSource, Model, Store } from '..'
|
|
2
|
+
import { TestUser } from './mocks/test-user'
|
|
3
|
+
|
|
4
|
+
describe( 'Json DataSource', ()=>{
|
|
5
|
+
let datasource: JsonDataSource
|
|
6
|
+
const resolveDelay = 50
|
|
7
|
+
|
|
8
|
+
describe( 'Delayed promise resolution', ()=>{
|
|
9
|
+
|
|
10
|
+
beforeEach(()=>{
|
|
11
|
+
datasource = new JsonDataSource({
|
|
12
|
+
collection: { a: { id: 'a' }, b: { id: 'b' }, c: { id: 'c' } } as any
|
|
13
|
+
}).simulateDelay( resolveDelay )
|
|
14
|
+
Store.useDataSource( datasource )
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it( 'should fail if no wait to resolve', async ()=>{
|
|
18
|
+
let result: DocumentObject | undefined = undefined
|
|
19
|
+
datasource.findById( 'a', 'collection' ).then( data => result = data )
|
|
20
|
+
expect( result ).not.toBeDefined()
|
|
21
|
+
await datasource.wait()
|
|
22
|
+
expect( result ).toBeDefined()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it( 'should wait promises to resolve', async ()=>{
|
|
26
|
+
let result: DocumentObject | undefined = undefined
|
|
27
|
+
datasource.findById( 'a', 'collection' ).then( data => result = data )
|
|
28
|
+
await datasource.wait()
|
|
29
|
+
expect( result ).toBeDefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it( 'should accumulate promises', async ()=>{
|
|
33
|
+
datasource.findById( 'a', 'collection' )
|
|
34
|
+
datasource.findById( 'b', 'collection' )
|
|
35
|
+
datasource.findById( 'c', 'collection' )
|
|
36
|
+
const promises = await datasource.wait()
|
|
37
|
+
expect( promises ).toHaveLength( 3 )
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it( 'should remove resolved promises', ( done )=>{
|
|
41
|
+
datasource.findById( 'b', 'collection' )
|
|
42
|
+
datasource.findById( 'a', 'collection' )
|
|
43
|
+
setTimeout(
|
|
44
|
+
async ()=>{
|
|
45
|
+
datasource.findById( 'c', 'collection' )
|
|
46
|
+
const promises = await datasource.wait()
|
|
47
|
+
expect( promises ).toHaveLength( 1 )
|
|
48
|
+
done()
|
|
49
|
+
},
|
|
50
|
+
resolveDelay * 3
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it( 'should work with save', async ()=>{
|
|
55
|
+
datasource.save({ testCollection: [{ id: "id" } as any ]})
|
|
56
|
+
await datasource.wait()
|
|
57
|
+
expect( datasource.rawData ).toEqual( expect.objectContaining({
|
|
58
|
+
testCollection: { id: { id: "id" } }
|
|
59
|
+
}))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it( 'should work with model', async ()=>{
|
|
63
|
+
const model = Store.getModel( 'TestUser' )
|
|
64
|
+
model.save( new TestUser('id456') )
|
|
65
|
+
await datasource.wait()
|
|
66
|
+
expect( datasource.rawData.TestUser ).toEqual( expect.objectContaining({
|
|
67
|
+
id456: expect.anything()
|
|
68
|
+
}))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe( 'Error simulation', ()=>{
|
|
74
|
+
let model: Model<TestUser>
|
|
75
|
+
|
|
76
|
+
beforeAll(()=>{
|
|
77
|
+
datasource = new JsonDataSource({
|
|
78
|
+
collection: { a: { id: 'a' }, b: { id: 'b' }, c: { id: 'c' } } as any
|
|
79
|
+
}).simulateError( 'Simulated error' )
|
|
80
|
+
Store.useDataSource( datasource )
|
|
81
|
+
model = Store.getModel<TestUser>( 'TestUser' )
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it( 'should simulate error on findById', ()=>{
|
|
85
|
+
expect( model.findById( 'a' ) ).rejects.toThrow( 'Simulated error' )
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it( 'should simulate error on find', ()=>{
|
|
89
|
+
expect( model.find().get() ).rejects.toThrow( 'Simulated error' )
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it( 'should simulate error on save', ()=>{
|
|
93
|
+
expect( model.save( new TestUser('id') ) ).rejects.toThrow( 'Simulated error' )
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it( 'should simulate error on delete', ()=>{
|
|
97
|
+
expect( model.delete( 'b' ) ).rejects.toThrow( 'Simulated error' )
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { Collections, Persistent, PersistentObject } from '../persistent/persistent';
|
|
2
|
+
import { DataSource, DocumentObject, QueryObject, QueryOperation } from "./data-source";
|
|
3
|
+
|
|
4
|
+
export interface JsonRawData {
|
|
5
|
+
[ collection: string ]: {
|
|
6
|
+
[ documentId: string ]: PersistentObject<Persistent>
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ErrorOnOperation {
|
|
11
|
+
store: string
|
|
12
|
+
find: string
|
|
13
|
+
findById: string
|
|
14
|
+
delete: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type QueryProcessors = {
|
|
18
|
+
[ P in keyof Required<QueryObject<unknown>> ]: Function
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A concrete implementation of the DataSource interface uses an in-memory data store
|
|
23
|
+
* initialized by a JSON object.
|
|
24
|
+
* It is useful for testing purposes.
|
|
25
|
+
* The data in the JSON object is not persisted.
|
|
26
|
+
*/
|
|
27
|
+
export class JsonDataSource implements DataSource {
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param jsonRawData the JSON object to be used as data store
|
|
31
|
+
*/
|
|
32
|
+
constructor( jsonRawData?: JsonRawData ) {
|
|
33
|
+
if ( jsonRawData ) this._jsonRawData = jsonRawData;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set the JSON object to initialize the data store. Use to set the it after
|
|
38
|
+
* the constructor has been called.
|
|
39
|
+
* @param jsonRawData the JSON object to be used as data store
|
|
40
|
+
*/
|
|
41
|
+
setDataStore( rawDataStore: JsonRawData ) {
|
|
42
|
+
this._jsonRawData = rawDataStore;
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Introduce a delay in the execution of operations to simulate a real data source
|
|
48
|
+
* @param miliSeconds the number of milliseconds to delay the execution of operations
|
|
49
|
+
* @returns a chainable reference to this object
|
|
50
|
+
*/
|
|
51
|
+
simulateDelay( miliSeconds: number ) {
|
|
52
|
+
this._simulateDelay = miliSeconds
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
findById( id: string, collectionName: string ): Promise< DocumentObject > {
|
|
57
|
+
if ( this._simulateError?.findById ) throw new Error( this._simulateError.findById )
|
|
58
|
+
|
|
59
|
+
return this.resolveWithDelay( this._jsonRawData[ collectionName ]?.[ id ] )
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
save( collections: Collections ): Promise< void > {
|
|
63
|
+
if ( this._simulateError?.store ) throw new Error( this._simulateError.store )
|
|
64
|
+
|
|
65
|
+
Object.entries( collections ).forEach(([ collectionName, collection ]) => {
|
|
66
|
+
if ( !this._jsonRawData[ collectionName ] ) this._jsonRawData[ collectionName ] = {}
|
|
67
|
+
collection?.forEach( document => {
|
|
68
|
+
this._jsonRawData[ collectionName ]![ document.id ] = document
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return this.resolveWithDelay()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
find( queryObject: QueryObject<DocumentObject>, collectionName: string ): Promise< DocumentObject[] > {
|
|
76
|
+
if ( this._simulateError?.find ) throw new Error( this._simulateError.find )
|
|
77
|
+
|
|
78
|
+
const rawDataArray = Object.values( this._jsonRawData[ collectionName ] || {} )
|
|
79
|
+
if ( !queryObject ) return this.resolveWithDelay( rawDataArray )
|
|
80
|
+
|
|
81
|
+
this._lastLimit = queryObject.limit || 0
|
|
82
|
+
this._cursor = 0
|
|
83
|
+
|
|
84
|
+
this._lastMatchingDocs = Object.entries( queryObject ).reduce(
|
|
85
|
+
( prevDocs, [ processMethod, value ]) => {
|
|
86
|
+
|
|
87
|
+
return this.queryProcessor( prevDocs, processMethod as any, value )
|
|
88
|
+
|
|
89
|
+
}, Object.values( rawDataArray )
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return this.resolveWithDelay( this._lastMatchingDocs.slice( 0, queryObject.limit ) )
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
delete( id: string, collectionName: string ): Promise<void> {
|
|
96
|
+
if ( this._simulateError?.delete ) throw new Error( this._simulateError.delete )
|
|
97
|
+
|
|
98
|
+
delete this._jsonRawData[ collectionName ]![ id ]
|
|
99
|
+
return this.resolveWithDelay()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
next( limit?: number ): Promise< DocumentObject[] > {
|
|
103
|
+
if ( limit ) this._lastLimit = limit
|
|
104
|
+
this.incCursor( this._lastLimit )
|
|
105
|
+
|
|
106
|
+
return this.resolveWithDelay( this._lastMatchingDocs.slice( this._cursor, this._cursor + this._lastLimit ) )
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
count( queryObject: QueryObject<DocumentObject>, collectionName: string ): Promise<number> {
|
|
110
|
+
return this.resolveWithDelay(
|
|
111
|
+
Object.keys( this._jsonRawData[ collectionName ] ?? {} ).length
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @returns the raw data store data as a JSON object
|
|
117
|
+
*/
|
|
118
|
+
get rawData() {
|
|
119
|
+
return this._jsonRawData
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Wait for all pending promises to be resolved
|
|
124
|
+
* @returns a promise that resolves when all pending promises are resolved
|
|
125
|
+
*/
|
|
126
|
+
wait() {
|
|
127
|
+
return Promise.all([ ...this._pendingPromises ])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private incCursor( amount: number ) {
|
|
131
|
+
this._cursor += amount
|
|
132
|
+
if ( this._cursor > this._lastMatchingDocs.length ) {
|
|
133
|
+
this._cursor = this._lastMatchingDocs.length
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
simulateError( error: string | ErrorOnOperation | undefined ): this {
|
|
138
|
+
if ( error === undefined ) {
|
|
139
|
+
this._simulateError = undefined
|
|
140
|
+
return this
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if ( typeof error === 'string' ) {
|
|
144
|
+
this._simulateError = {
|
|
145
|
+
store: error,
|
|
146
|
+
find: error,
|
|
147
|
+
findById: error,
|
|
148
|
+
delete: error
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else this._simulateError = error
|
|
152
|
+
|
|
153
|
+
return this
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private decCursor( amount: number ) {
|
|
157
|
+
this._cursor -= amount
|
|
158
|
+
if ( this._cursor < 0 ) {
|
|
159
|
+
this._cursor = 0
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private queryProcessor<T, P extends keyof QueryProcessors>( docs: DocumentObject[], processMethod: P, value: QueryObject<T>[P] ) {
|
|
166
|
+
|
|
167
|
+
const processors: QueryProcessors = {
|
|
168
|
+
|
|
169
|
+
limit: ( limit: number ) => docs, //.slice( 0, limit ),
|
|
170
|
+
|
|
171
|
+
operations: ( operations: QueryOperation<T>[] ) => this.retrieveQueryDocs( docs, operations ),
|
|
172
|
+
|
|
173
|
+
sort: ({ order, propertyName }) => docs.sort( ( a, b ) => {
|
|
174
|
+
if ( order === 'asc' ) {
|
|
175
|
+
return this.deepValue( a, propertyName ) > this.deepValue( b, propertyName )? 1 : -1
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return this.deepValue( a, propertyName ) < this.deepValue( b, propertyName )? 1 : -1
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return processors[ processMethod ]( value )
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private retrieveQueryDocs<T>( docs: DocumentObject[], queryOperations: QueryOperation<T>[] ): DocumentObject[] {
|
|
187
|
+
return queryOperations.reduce(( prevDocs, queryOperation, i ) => {
|
|
188
|
+
if ( queryOperation.aggregate ) {
|
|
189
|
+
const aggregate = docs.filter( doc => this.isQueryMatched( doc, queryOperation ) )
|
|
190
|
+
if ( i === 0 ) return aggregate
|
|
191
|
+
else return prevDocs.concat( aggregate )
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
return prevDocs.filter( doc => this.isQueryMatched( doc, queryOperation ) )
|
|
195
|
+
}
|
|
196
|
+
}, docs )
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private deepValue( obj: {}, propertyPath: string /*like person.name.firstName*/) {
|
|
200
|
+
const propChain = propertyPath.split( '.' )
|
|
201
|
+
return propChain.reduce(( value, prop ) => value[ prop ], obj )
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private isQueryMatched<T>( doc: DocumentObject, queryOperation: QueryOperation<T> ) {
|
|
205
|
+
const queryOperator = {
|
|
206
|
+
'==': <U>(a: U, b: U) => a === b,
|
|
207
|
+
'!=': <U>(a: U, b: U) => a !== b,
|
|
208
|
+
'<': <U>(a: U, b: U) => a < b,
|
|
209
|
+
'<=': <U>(a: U, b: U) => a <= b,
|
|
210
|
+
'>': <U>(a: U, b: U) => a > b,
|
|
211
|
+
'>=': <U>(a: U, b: U) => a >= b,
|
|
212
|
+
'containsAny': <U>(a: U[], b: U[]) => a?.some( v => b?.includes( v ) ),
|
|
213
|
+
'contains': <U>(a: U[], b: U) => a?.includes( b ),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { property, value, operator } = queryOperation
|
|
217
|
+
const [ propValue, v ] = this.retrieveValuesToCompare( doc, property as string, value )
|
|
218
|
+
|
|
219
|
+
return queryOperator[ operator ]( propValue, v )
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private retrieveValuesToCompare( doc: DocumentObject, propertyName: string, value: unknown ): [ any, any ] {
|
|
223
|
+
const propertyValue = doc[ propertyName ]
|
|
224
|
+
|
|
225
|
+
if ( propertyValue && typeof value === 'object' && !Array.isArray( value )) {
|
|
226
|
+
const propName = Object.keys( value! )[0]!
|
|
227
|
+
var [ propVal, val ] = this.retrieveValuesToCompare( propertyValue, propName, value?.[ propName ] )
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return [ propVal || propertyValue, val || value ]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private resolveWithDelay<T>( data?: T ): Promise<T> {
|
|
234
|
+
if ( this._simulateDelay <=0 ) return Promise.resolve( data! )
|
|
235
|
+
|
|
236
|
+
const promise = new Promise<T>( resolve => {
|
|
237
|
+
setTimeout(
|
|
238
|
+
()=> resolve( data! ),
|
|
239
|
+
this._simulateDelay
|
|
240
|
+
)
|
|
241
|
+
})
|
|
242
|
+
this._pendingPromises.push( promise )
|
|
243
|
+
promise.finally(
|
|
244
|
+
()=> this._pendingPromises = this._pendingPromises.filter( p => p === promise )
|
|
245
|
+
)
|
|
246
|
+
return promise
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private _jsonRawData: JsonRawData = {}
|
|
250
|
+
private _lastMatchingDocs: DocumentObject[] = []
|
|
251
|
+
private _lastLimit: number = 0
|
|
252
|
+
private _cursor: number = 0
|
|
253
|
+
private _simulateDelay: number = 0
|
|
254
|
+
private _pendingPromises: Promise<any>[] = []
|
|
255
|
+
private _simulateError: ErrorOnOperation | undefined
|
|
256
|
+
}
|