dataflux 1.0.2 → 1.1.2
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/README.md +199 -130
- package/dist/Model.js +198 -59
- package/dist/ObserverStore.js +178 -131
- package/dist/PersistentStore.js +104 -75
- package/dist/PubSub.js +59 -0
- package/dist/ReactStore.js +22 -2
- package/dist/Store.js +295 -151
- package/dist/fingerprint.js +8 -12
- package/dist/modelHooksUtils.js +31 -14
- package/package.json +17 -8
package/README.md
CHANGED
|
@@ -13,13 +13,22 @@ DataFlux is a JavaScript library that automatically interfaces with your REST AP
|
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
+
Using npm:
|
|
16
17
|
```sh
|
|
17
18
|
npm install dataflux
|
|
18
19
|
```
|
|
19
20
|
|
|
21
|
+
Using jsDelivr CDN:
|
|
22
|
+
```html
|
|
23
|
+
<script src="https://cdn.jsdelivr.net/npm/dataflux/dist/index.js"></script>
|
|
24
|
+
```
|
|
25
|
+
|
|
20
26
|
## Examples
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
Create your global store by creating a file (e.g., named `store.js`) containing the model declaration.
|
|
29
|
+
|
|
30
|
+
Consider the following hypothetical store/model declaration common to all the examples below:
|
|
31
|
+
|
|
23
32
|
```js
|
|
24
33
|
import {Store, Model} from "dataflux";
|
|
25
34
|
|
|
@@ -30,16 +39,27 @@ const book = new Model("book", `https://rest.example.net/api/v1/books`);
|
|
|
30
39
|
store.addModel(author);
|
|
31
40
|
store.addModel(book);
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
// An object relation between author.id and book.authorId as follows
|
|
43
|
+
author.addRelation(book, "id", "authorId");
|
|
44
|
+
|
|
45
|
+
export default store;
|
|
34
46
|
```
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
The store can be initialized with [various options](#configuration). You need only one store for the entire application, that's why you should declare it in its own file and import it in multiple places.
|
|
50
|
+
|
|
51
|
+
The creation of a model requires at least a name and an url. GET, POST, PUT, and DELETE operations are going to be performed against the same url. [Models can be created with considerably more advanced options.](#models-creation)
|
|
52
|
+
|
|
53
|
+
A JS object is automatically created for each item returned by the API, for each model. The object has the same properties of the JSON item plus some high-level method (see [objects methods](#objects-methods)).
|
|
54
|
+
**All the objects are indexed in the store.**
|
|
37
55
|
|
|
38
56
|
### Example 1
|
|
39
57
|
|
|
40
58
|
Retrieve and edit an author not knowing the ID:
|
|
41
59
|
|
|
42
60
|
```js
|
|
61
|
+
import store from "./store";
|
|
62
|
+
|
|
43
63
|
// Find the author Dante Alighieri
|
|
44
64
|
store.find("author", ({name, surname}) => name == "Dante" && surname == "Alighieri")
|
|
45
65
|
.then(([author]) => {
|
|
@@ -148,9 +168,9 @@ class MyComponent extends React.Component {
|
|
|
148
168
|
componentDidMount() {
|
|
149
169
|
// Get all books with a price < 20
|
|
150
170
|
store.findAll("book", "books", this, ({price}) => price < 20);
|
|
151
|
-
// Every time the dataset changes, a setState will be automatically
|
|
152
|
-
// An attribute "books" will be added/updated in the
|
|
153
|
-
// state remains unchanged).
|
|
171
|
+
// Every time the dataset changes, a setState will be automatically
|
|
172
|
+
// performed. An attribute "books" will be added/updated in the
|
|
173
|
+
// state (the rest of the state remains unchanged).
|
|
154
174
|
|
|
155
175
|
// findAll is a syntactic sugar for:
|
|
156
176
|
// const callback = (books) => {this.setState({...this.state, books})};
|
|
@@ -160,19 +180,23 @@ class MyComponent extends React.Component {
|
|
|
160
180
|
render(){
|
|
161
181
|
const {books} = this.state;
|
|
162
182
|
|
|
163
|
-
return books.map(book =>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
return books.map(book =>
|
|
184
|
+
<Book
|
|
185
|
+
onTitleChange={(title) => book.set("title", title)}
|
|
186
|
+
// onTitleChange will alter the book and so the current
|
|
187
|
+
// state of "books" (a setState will be performed).
|
|
188
|
+
//
|
|
189
|
+
// Alternatively:
|
|
190
|
+
// onTitleChange={store.handleChange(book, "title")}
|
|
191
|
+
// is a syntactic sugar of the function above
|
|
192
|
+
/>);
|
|
171
193
|
}
|
|
172
194
|
}
|
|
173
195
|
```
|
|
174
196
|
|
|
175
|
-
|
|
197
|
+
The method `findAll` returns always an array. The method `findOne` returns a single object (if multiple objects satisfy the search, the first is returned).
|
|
198
|
+
|
|
199
|
+
When the component will unmount, the `findAll` subscription will be automatically terminated without the need to unsubscribe. Be aware, `store.findAll` injects the unsubscribe call inside `componentWillUnmount`. If your component already implements `componentWillUnmount()`, then you will have to use `store.subscribe` and `store.unsubscribe` instead of `store.findAll`, to avoid side effects when the component is unmounted.
|
|
176
200
|
|
|
177
201
|
## Configuration
|
|
178
202
|
|
|
@@ -187,107 +211,7 @@ The store can be configured with the following options:
|
|
|
187
211
|
|
|
188
212
|
|
|
189
213
|
|
|
190
|
-
##
|
|
191
|
-
The option `autoSave` can be `true`, `false`, a number (milliseconds).
|
|
192
|
-
|
|
193
|
-
* When `autoSave` is set to `false`, the following operations are equivalent:
|
|
194
|
-
```js
|
|
195
|
-
object.set("name", "Dante");
|
|
196
|
-
|
|
197
|
-
object.name = "Dante";
|
|
198
|
-
```
|
|
199
|
-
No matter which of the two approaches you use, the command `store.save()` must be invoked to sync the changes with the server.
|
|
200
|
-
|
|
201
|
-
> The command `store.save()` is always able to recognize changed objects that need to be persisted.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
* **When `autoSave` is set to `true`, the above operations are NOT equivalent.**
|
|
205
|
-
|
|
206
|
-
Using `.set(attribute, value)` informs the store that an object changed, while changing directly a property the object (`object.name = "Dante"`) does not. Since the store is not aware of the changes, they will not be synced with the server. **To avoid this, always use `.set(attribute, value)`.**
|
|
207
|
-
|
|
208
|
-
> The commands `store.insert()`, `store.delete()`, and `object.destroy()` are always visible to the store, and so syncing is always performed when `autoSave` is `true`.
|
|
209
|
-
|
|
210
|
-
* **When `autoSave` is set to an amount of milliseconds, the above operations are still NOT equivalent, but...**
|
|
211
|
-
|
|
212
|
-
The store will perform as if the `autoSave` was set to `true`; hence, changes performed with `.set(attribute, value)` are synced. However, it will periodically attempt also a `store.save()`. Since `store.save()` is always able to recognize edited objects, also changes directly operated on a property of the object (`object.name = "Dante"`) are synced.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
## API interaction
|
|
216
|
-
DataFlux is able to identify three sets of objects: inserted, updated, deleted.
|
|
217
|
-
Each of these set is synced to the server with POST, PUT, and DELETE REST operations, respectively.
|
|
218
|
-
|
|
219
|
-
The interaction with the API is handled automatically, multiple requests are prevented and operations are bundled as much as possible.
|
|
220
|
-
|
|
221
|
-
For example (with autoSave):
|
|
222
|
-
```js
|
|
223
|
-
store.find('book', (book) => book.price < 20);
|
|
224
|
-
store.find('book', (book) => book.price > 60);
|
|
225
|
-
// The commands above will correspond to 1 single query to the REST API.
|
|
226
|
-
|
|
227
|
-
author1.set("name", "Dante");
|
|
228
|
-
author2.set("name", "Italo");
|
|
229
|
-
author3.set("name", "Umberto");
|
|
230
|
-
author4.name = "Primo";
|
|
231
|
-
// The commands above will correspond to 1 single query to the REST API,
|
|
232
|
-
// no matter how many editing operations.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
author1.set("name", "Dante");
|
|
236
|
-
setTimeout(() => author2.set("name", "Italo"), 10000); // To "emulate" a user interaction.
|
|
237
|
-
// The commands above will correspond to 2 queries to the REST API
|
|
238
|
-
|
|
239
|
-
const author1 = {surname: "Alighieri"};
|
|
240
|
-
store.insert(author1);
|
|
241
|
-
author1.set("name", "Dante");
|
|
242
|
-
author1.delete(author1);
|
|
243
|
-
// The commands above will not produce any query to the REST API since
|
|
244
|
-
// the initial and final states of the store are the same (object created and removed).
|
|
245
|
-
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### REST API format
|
|
249
|
-
|
|
250
|
-
The APIs must return/accept an array of JSON objects or a single object. If your API uses a different format, use a function in the [model creation](#model-creation) to transform the data.
|
|
251
|
-
|
|
252
|
-
The following format is automatically accepted, and it will create two objects.
|
|
253
|
-
|
|
254
|
-
```json
|
|
255
|
-
[
|
|
256
|
-
{
|
|
257
|
-
"name": "Dante",
|
|
258
|
-
"surname": "Alighieri",
|
|
259
|
-
"reviews": [...]
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
"name": "Giovanni",
|
|
263
|
-
"surname": "Boccaccio",
|
|
264
|
-
"reviews": [...]
|
|
265
|
-
}
|
|
266
|
-
]
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
The following format is automatically accepted, and it will create one object.
|
|
270
|
-
|
|
271
|
-
```json
|
|
272
|
-
{
|
|
273
|
-
"username": "Massimo",
|
|
274
|
-
"website": "https://massimocandela.com",
|
|
275
|
-
"otherParameters": {
|
|
276
|
-
...
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
The following format will create a single object, which probably you don't want. Use a function in [model creation](#model-creation) to unwrap the data.
|
|
282
|
-
|
|
283
|
-
```json
|
|
284
|
-
{
|
|
285
|
-
"books": [],
|
|
286
|
-
"authors": []
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
## Model creation
|
|
214
|
+
## Models creation
|
|
291
215
|
|
|
292
216
|
A model can be simply created with:
|
|
293
217
|
|
|
@@ -297,7 +221,7 @@ const book = new Model("book", `https://rest.example.net/api/v1/books`);
|
|
|
297
221
|
|
|
298
222
|
However, in many cases more complex APIs require different settings for the various operations.
|
|
299
223
|
|
|
300
|
-
Instead of an url, you can pass options to perform more elaborated Model's
|
|
224
|
+
Instead of an url, you can pass options to perform more elaborated Model's initializations.
|
|
301
225
|
|
|
302
226
|
```js
|
|
303
227
|
const options = {
|
|
@@ -323,7 +247,7 @@ const book = new Model("book", options);
|
|
|
323
247
|
```
|
|
324
248
|
You don't necessarily need to specify a url for each operation. If a url is not specified for an operation, the url defined for the `GET` operation is used.
|
|
325
249
|
|
|
326
|
-
For example, if you want to perform inserts and updates with
|
|
250
|
+
For example, if you want to perform both inserts and updates with `PUT`, you can do:
|
|
327
251
|
```js
|
|
328
252
|
const options = {
|
|
329
253
|
retrieve: {
|
|
@@ -360,7 +284,18 @@ const options = {
|
|
|
360
284
|
const book = new Model("book", options);
|
|
361
285
|
```
|
|
362
286
|
|
|
363
|
-
|
|
287
|
+
All the possible options for a model creation are:
|
|
288
|
+
|
|
289
|
+
| Name | Description |
|
|
290
|
+
|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
291
|
+
| retrieve | Describes the operation to retrieve a collection of objects from a REST API. It can be an object containing `method` and `url` or a function. See examples above. |
|
|
292
|
+
| insert | Describes the operation to insert a new object in the collection. It can be an object containing `method` and `url` or a function. See examples above. |
|
|
293
|
+
| update | Describes the operation to update objects of the collection. It can be an object containing `method` and `url` or a function. See examples above. |
|
|
294
|
+
| delete | Describes the operation to remove objects from the collection. It can be an object containing `method` and `url` or a function. See examples above. |
|
|
295
|
+
| axios | It allows to specify an axios instance to be used for the queries. If not specified, a new one will be used. |
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
### Model relations
|
|
364
299
|
|
|
365
300
|
Optionally, you can create relations among models.
|
|
366
301
|
|
|
@@ -374,15 +309,30 @@ author.addRelation(book, "id", "authorId");
|
|
|
374
309
|
```
|
|
375
310
|
In this example, we added an explicit relation between `author.id` and `book.authorId`. This means that the store will return as books belonging to the author, all the books having `authorId` equals to the id of the author.
|
|
376
311
|
|
|
377
|
-
|
|
312
|
+
|
|
313
|
+
Other ways to declare relations:
|
|
314
|
+
|
|
315
|
+
* `account.addRelation("user", "userId")`
|
|
316
|
+
|
|
317
|
+
When the third parameter is missing, it defaults to "id" (i.e., it is the shorter version of `account.addRelation("user", "userId", "id")`). This means that the store will return as user of the account, the user having `id` equals to `account.userId`.
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
* `author.addRelation("book", filterFunction)`
|
|
321
|
+
|
|
322
|
+
When the second parameter is a function, the function will be used by the store to filter the objects of the connected model. The `filterFunction` receives two parameters `(parentObject, possibleChildObject)` and returns a boolean. In this way you can create complex relations; e.g., a `filterFunction` equal to `(author, book) => author.name == book.authorName && author.surname == book.authorSurname` creates a relation based on two attributes.
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
#### Accessing model relations
|
|
326
|
+
|
|
327
|
+
Once the relation between the author and the book models is declared, all the author objects will expose a method `getRelation(type, filterFunction)` that can be used to retrieve a relation associated with the author. The `type` defines the model type (in our case, 'book'), the `filterFunction` is an optional parameter that can be passed in case the output needs an additional filtering.
|
|
378
328
|
|
|
379
329
|
For example, imagine you have the `author1` object defined in the examples above (Dante Alighieri):
|
|
380
330
|
|
|
381
331
|
```js
|
|
382
332
|
author1.getRelation("book")
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
333
|
+
.then(dantesBooks => {
|
|
334
|
+
// Do something with Dante's books
|
|
335
|
+
});
|
|
386
336
|
|
|
387
337
|
// Or..
|
|
388
338
|
author1.getRelation("book", (book) => book.price < 20)
|
|
@@ -390,14 +340,133 @@ author1.getRelation("book", (book) => book.price < 20)
|
|
|
390
340
|
// Do something with Dante's books cheaper than 20
|
|
391
341
|
});
|
|
392
342
|
```
|
|
343
|
+
## Store methods
|
|
344
|
+
|
|
345
|
+
The store has the following method.
|
|
346
|
+
|
|
347
|
+
| Method | Description |
|
|
348
|
+
|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
349
|
+
| addModel(model) | Introduce a new model to the store. If lazyLoad = false (default), the model is populated with the objects coming from the API. |
|
|
350
|
+
| get(type, id) | It allows to retrieve an object based on its type and store's ID (see `getId()` in [objects methods](#objects-methods). The type is the name of the model. |
|
|
351
|
+
| find(type,filterFunction) | The promise-oriented method to access objects given a type and a filter function. See [example 1](#example-1). |
|
|
352
|
+
| delete(objects) | It deletes an array of objects. See [example 1](#example-3). |
|
|
353
|
+
| delete(type, filterFunction) | It deleted objects given an array and a filter function. See [example 1](#example-3). |
|
|
354
|
+
| insert(type, object) | It creates a new object of a given type and inserts it in the store. |
|
|
355
|
+
|
|
356
|
+
## Objects methods
|
|
357
|
+
Each object created is enriched with the following methods.
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
| Method | Description |
|
|
361
|
+
|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
362
|
+
| getId() | It returns a unique ID used by the store to identify the object. The ID is unique inside a single model. Be aware, `object.id` and `objet.getId()` may return different values, since store's IDs can be different from the one of the REST API. |
|
|
363
|
+
| set(attribute, value) | A method to set an attribute to the object. It provides some advantages compared to doing `object.attribute = value`, these are discussed in [below](#editing-objects). |
|
|
364
|
+
| save() | Method to save the object. You can do `store.save()` instead. |
|
|
365
|
+
| destroy() | Method to delete the object. You can do `store.delete()` instead. |
|
|
366
|
+
| get(attribute) | If you like symmetry and since you do `.set()` you would like to do also `.get()` of the attributes. It does not provide any advantage compared to accessing directly the attribute (e.g., `author.name`). |
|
|
367
|
+
| getRelation(model, filterFunction) | To get all the objects respecting a specific relation with this object (see [model relations](#model-relations)). |
|
|
368
|
+
| toJSON() | It returns a pure JSON representation of the object. |
|
|
369
|
+
| toString() | It returns a string representation of the object. |
|
|
370
|
+
| getFingerprint() | It returns a hash of the object. The hash changes at every change of the object or of any nested object. Useful to detect object changes. |
|
|
371
|
+
| getModel() | It returns the model of this object. Mostly useful to do `object.getModel().getType()` and obtain a string defining the type of the object. |
|
|
393
372
|
|
|
394
|
-
|
|
373
|
+
## Editing objects
|
|
374
|
+
The option `autoSave` can be `true`, `false`, or a number (milliseconds).
|
|
395
375
|
|
|
396
|
-
* `
|
|
397
|
-
|
|
398
|
-
|
|
376
|
+
* When `autoSave` is set to `false`, the following operations are equivalent:
|
|
377
|
+
```js
|
|
378
|
+
object.set("name", "Dante");
|
|
379
|
+
|
|
380
|
+
object.name = "Dante";
|
|
381
|
+
```
|
|
382
|
+
No matter which of the two approaches you use, the command `store.save()` must be invoked to sync the changes with the server.
|
|
383
|
+
|
|
384
|
+
> The command `store.save()` is always able to recognize changed objects that need to be persisted.
|
|
399
385
|
|
|
400
386
|
|
|
401
|
-
* `
|
|
387
|
+
* **When `autoSave` is set to `true`, the above operations are NOT equivalent.**
|
|
388
|
+
|
|
389
|
+
Using `.set(attribute, value)` informs the store that an object changed, while changing directly an attribute of the object (`object.name = "Dante"`) does not. Since the store is not aware of the changes, they will not be synced with the server. **To avoid this, always use `.set(attribute, value)`.**
|
|
390
|
+
|
|
391
|
+
> The commands `store.insert()`, `store.delete()`, and `object.destroy()` are always visible to the store, and so syncing is always performed when `autoSave` is `true`.
|
|
392
|
+
|
|
393
|
+
* **When `autoSave` is set to an amount of milliseconds, the above operations are still NOT equivalent, but...**
|
|
394
|
+
|
|
395
|
+
The store will perform as if the `autoSave` was set to `true`; hence, changes performed with `.set(attribute, value)` are synced. However, it will periodically attempt also a `store.save()`. Since `store.save()` is always able to recognize edited objects, also changes directly operated on an attribute of the object (`object.name = "Dante"`) are synced.
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
## API interaction
|
|
400
|
+
DataFlux is able to identify three sets of objects: inserted, updated, deleted.
|
|
401
|
+
Each of these set is synced with the server with POST, PUT, and DELETE REST operations, respectively.
|
|
402
402
|
|
|
403
|
-
|
|
403
|
+
The interaction with the API is handled automatically, multiple requests are prevented and operations are bundled as much as possible.
|
|
404
|
+
|
|
405
|
+
For example (with autoSave):
|
|
406
|
+
```js
|
|
407
|
+
store.find('book', (book) => book.price < 20);
|
|
408
|
+
store.find('book', (book) => book.price > 60);
|
|
409
|
+
// The commands above will correspond to 1 single query to the REST API.
|
|
410
|
+
|
|
411
|
+
author1.set("name", "Dante");
|
|
412
|
+
author2.set("name", "Italo");
|
|
413
|
+
author3.set("name", "Umberto");
|
|
414
|
+
author4.name = "Primo";
|
|
415
|
+
// The commands above will correspond to 1 single query to the REST API,
|
|
416
|
+
// no matter how many editing operations.
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
author1.set("name", "Dante");
|
|
420
|
+
setTimeout(() => author2.set("name", "Italo"), 10000); // To "emulate" a user interaction.
|
|
421
|
+
// The commands above will correspond to 2 queries to the REST API
|
|
422
|
+
|
|
423
|
+
const author1 = {surname: "Alighieri"};
|
|
424
|
+
store.insert(author1);
|
|
425
|
+
author1.set("name", "Dante");
|
|
426
|
+
store.delete(author1);
|
|
427
|
+
// The commands above will not produce any query to the REST API since
|
|
428
|
+
// the initial and final states of the store are the same (object created and removed).
|
|
429
|
+
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### REST API format
|
|
433
|
+
|
|
434
|
+
The APIs must return/accept an array of JSON objects or a single object. If your API uses a different format, use a function in the [models creation](#models-creation) to transform the data.
|
|
435
|
+
|
|
436
|
+
The following format is automatically accepted, and it will create two objects.
|
|
437
|
+
|
|
438
|
+
```json
|
|
439
|
+
[
|
|
440
|
+
{
|
|
441
|
+
"name": "Dante",
|
|
442
|
+
"surname": "Alighieri",
|
|
443
|
+
"reviews": [...]
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"name": "Giovanni",
|
|
447
|
+
"surname": "Boccaccio",
|
|
448
|
+
"reviews": [...]
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The following format is automatically accepted, and it will create one object.
|
|
454
|
+
|
|
455
|
+
```json
|
|
456
|
+
{
|
|
457
|
+
"username": "Massimo",
|
|
458
|
+
"website": "https://massimocandela.com",
|
|
459
|
+
"otherParameters": {
|
|
460
|
+
...
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
The following format will create a single object, which probably you don't want. Use a function in [models creation](#models-creation) to unwrap the data.
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
{
|
|
469
|
+
"books": [],
|
|
470
|
+
"authors": []
|
|
471
|
+
}
|
|
472
|
+
```
|