dataflux 1.0.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/.babelrc +20 -0
- package/LICENSE +21 -0
- package/README.md +403 -0
- package/dist/Model.js +175 -0
- package/dist/ObserverStore.js +260 -0
- package/dist/PersistentStore.js +148 -0
- package/dist/ReactStore.js +119 -0
- package/dist/Store.js +208 -0
- package/dist/StoreObject.js +96 -0
- package/dist/fingerprint.js +67 -0
- package/dist/index.js +23 -0
- package/dist/modelHooksUtils.js +78 -0
- package/package.json +90 -0
package/.babelrc
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"presets": [
|
|
3
|
+
"@babel/preset-env"
|
|
4
|
+
],
|
|
5
|
+
"plugins": [
|
|
6
|
+
"@babel/plugin-proposal-class-properties",
|
|
7
|
+
"@babel/plugin-transform-async-to-generator",
|
|
8
|
+
"@babel/plugin-proposal-object-rest-spread"
|
|
9
|
+
],
|
|
10
|
+
|
|
11
|
+
"ignore": [
|
|
12
|
+
"./node_modules",
|
|
13
|
+
"./assets",
|
|
14
|
+
"./view",
|
|
15
|
+
"./tests",
|
|
16
|
+
"./logs",
|
|
17
|
+
"./dist",
|
|
18
|
+
"./build"
|
|
19
|
+
]
|
|
20
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Massimo Candela
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
## DataFlux
|
|
2
|
+
|
|
3
|
+
DataFlux is a JavaScript library that automatically interfaces with your REST APIs to create a 2-way-synced local data store. If used with React, it transparently manages data propagation in the state.
|
|
4
|
+
|
|
5
|
+
* **Automated:** Given a collection of urls pointing to REST APIs, it creates a data layer (called `store`) able to retrieve, insert, update, delete the objects returned by the API. When objects are edited by the client, the store detects the edited objects and dispatches targeted updates to the APIs. You will **work on local JS objects** (e.g., you can do `myObject.name = "test"`, or `myObject.destroy()`) and **ignore the synchronization with the server** that will happen automagically.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
* **Observable:** Queries to the store are observable. If you ask the store one or more objects (e.g., a list of books you want to display on your website), the store will track what subset of data you are using and push updates every time any of the object in the subset is subject to a change (e.g., a title of a book displayed on your page is edited or a new book matching the search criteria is added). **This is extremely useful with React!**
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
* **Full control:** If you don't like the store to manage the ORM operations automatically, you can set `autoSave: false` and explicitly tell the store when to save (i.e., `store.save()`). Additionally, you can control the single objects individually (e.g., `myObject.save()`). You can also set `lazyLoad: true` and only retrieve the data from the API when requested (e.g., if you never search for books, these will never be retrieved)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install dataflux
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Examples
|
|
21
|
+
|
|
22
|
+
Consider the following store/model declaration common to all the examples below:
|
|
23
|
+
```js
|
|
24
|
+
import {Store, Model} from "dataflux";
|
|
25
|
+
|
|
26
|
+
const store = new Store();
|
|
27
|
+
const author = new Model("author", `https://rest.example.net/api/v1/authors`);
|
|
28
|
+
const book = new Model("book", `https://rest.example.net/api/v1/books`);
|
|
29
|
+
|
|
30
|
+
store.addModel(author);
|
|
31
|
+
store.addModel(book);
|
|
32
|
+
|
|
33
|
+
author.addRelation(book, "id", "authorId"); // Add an object relation between author.id and book.authorId
|
|
34
|
+
```
|
|
35
|
+
The store can be initialized with [various options](#configuration).
|
|
36
|
+
The creation of a model requires at least a name and an url. GET, POST, PUT, DELETE operations are going to be performed against the same url. [Models can be created with considerably more advanced options.](#model-creation)
|
|
37
|
+
|
|
38
|
+
### Example 1
|
|
39
|
+
|
|
40
|
+
Retrieve and edit an author not knowing the ID:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// Find the author Dante Alighieri
|
|
44
|
+
store.find("author", ({name, surname}) => name == "Dante" && surname == "Alighieri")
|
|
45
|
+
.then(([author]) => {
|
|
46
|
+
author.set("country", "Italy");
|
|
47
|
+
author.set("type", "poet");
|
|
48
|
+
// Nothing else to do, the store does a single PUT request to the model's API about the edited object
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> You don't necessarily need to use `object.set` to edit an object attribute. You could do `author.country = "Italy"`. However, this approach relies on a periodic detection of changes (while `.set` triggers an update immediately). Check the `autoSave` option for more information
|
|
53
|
+
|
|
54
|
+
### Example 2
|
|
55
|
+
|
|
56
|
+
Operations without autoSave:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// To disable autoSave you must declare the store as follows
|
|
60
|
+
const store = new Store({autoSave: false});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The same example above now becomes:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// Find the author Dante Alighieri
|
|
67
|
+
store.find("author", ({name, surname}) => name == "Dante" && surname == "Alighieri")
|
|
68
|
+
.then(([author]) => {
|
|
69
|
+
// When autoSave = false, you can still use author.set, but there is no actual benefit
|
|
70
|
+
author.country = "Italy"
|
|
71
|
+
author.type = "poet"
|
|
72
|
+
|
|
73
|
+
store.save(); // Even if we changed only one author, prefer always store.save() to author.save()
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Example 3
|
|
78
|
+
|
|
79
|
+
Insert and delete objects:
|
|
80
|
+
```js
|
|
81
|
+
// Remove all authors with a name starting with "A"
|
|
82
|
+
store.delete("author", ({name}) => name.startsWith("A"));
|
|
83
|
+
// Add a new author
|
|
84
|
+
store.insert("author", {name: "Jane", surname: "Austen"});
|
|
85
|
+
// If autoSave = false, remember to do store.save();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You can also destroy a single object
|
|
89
|
+
```js
|
|
90
|
+
author.destroy();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or destroy a collection of authors you already selected
|
|
94
|
+
```js
|
|
95
|
+
store.find("author", ({name}) => name.startsWith("A"))
|
|
96
|
+
.then(authors => {
|
|
97
|
+
store.delete(authors);
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
### Example 4
|
|
101
|
+
|
|
102
|
+
Get all books of an author:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
author.getRelation("book");
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Example 5 - Observability
|
|
109
|
+
|
|
110
|
+
If you use `subscribe` instead of `find`, you can provide a callback to be invoked when data is ready or there is a change in the data.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
const drawBooksCallback = (books) => {
|
|
115
|
+
// Do something with the books
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Get all books with a price < 20
|
|
119
|
+
store.subscribe("book", drawBooks, ({price}) => price < 20);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If now somewhere a book is inserted/deleted/edited:
|
|
123
|
+
* if the book has `price < 20`, `drawBooksCallback` will be called again with the new dataset;
|
|
124
|
+
* if the book has `price > 20`, `drawBooksCallback` will NOT be called again (because the new book doesn't impact our selection).
|
|
125
|
+
|
|
126
|
+
You can terminate the subscription with `store.unsubscribe()`:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const subKey = store.subscribe("book", drawBooks, ({price}) => price < 20); // Subscribe
|
|
130
|
+
|
|
131
|
+
store.unsubscribe(subKey); // Unsubscribe
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Example 6 - Observability + React
|
|
135
|
+
|
|
136
|
+
The integration with React is offered transparently when using the store inside a `React.Component`.
|
|
137
|
+
You can use two methods: `findOne`, and `findAll`.
|
|
138
|
+
|
|
139
|
+
> Since the store is able to detect changes deep in a nested structure, you will not have to worry about the component not re-rendering. Also, the setState will only be triggered when the next change of the dataset is really impacting your selection.
|
|
140
|
+
|
|
141
|
+
React Component example
|
|
142
|
+
```jsx
|
|
143
|
+
class MyComponent extends React.Component {
|
|
144
|
+
constructor(props) {
|
|
145
|
+
super(props);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
componentDidMount() {
|
|
149
|
+
// Get all books with a price < 20
|
|
150
|
+
store.findAll("book", "books", this, ({price}) => price < 20);
|
|
151
|
+
// Every time the dataset changes, a setState will be automatically performed.
|
|
152
|
+
// An attribute "books" will be added/updated in the state (the rest of the
|
|
153
|
+
// state remains unchanged).
|
|
154
|
+
|
|
155
|
+
// findAll is a syntactic sugar for:
|
|
156
|
+
// const callback = (books) => {this.setState({...this.state, books})};
|
|
157
|
+
// store.subscribe("book", callback, ({price}) => price < 20);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
render(){
|
|
161
|
+
const {books} = this.state;
|
|
162
|
+
|
|
163
|
+
return books.map(book => {
|
|
164
|
+
return <Book
|
|
165
|
+
// onTitleChange will alter the book and so the current state of "books"
|
|
166
|
+
onTitleChange={(title) => book.set("title", title)}
|
|
167
|
+
// alternatively, onTitleChange={store.handleChange(book, "title")}
|
|
168
|
+
// is a syntactic sugar of the function above
|
|
169
|
+
/>
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
When the component will unmount, the `findAll` subscription will be terminated.
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
The store can be configured with the following options:
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
| Option | Description | Default |
|
|
183
|
+
|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
|
|
184
|
+
| autoSave | It can be `true`, `false`, or an amount of milliseconds (integer). If `false`, you will have to perform `store.save()` manually. If `true`, the store will automatically perform `save()` when objects change. If an amount of milliseconds is provided, the objects are saved periodically AND when a change is detected. See [Editing objects](#editing-objects) for more information. | 3000 |
|
|
185
|
+
| saveDelay | An amount of milliseconds used to defer synching operations with the server. It triggers `store.save()` milliseconds after the last change on the store's objects is detedect. This allows to bundle together multiple changes operated by an interacting user. See [Editing objects](#editing-objects) for more information. | 1000 |
|
|
186
|
+
| lazyLoad | A boolean. If set to `false`, the store is pre-populated with all the models' objects. If set to `true`, models' objects are loaded only on first usage (e.g., 'find', 'subscribe', 'getRelation'). LazyLoad operates per model, only the objects of the used models are loaded. | false |
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
## Editing objects
|
|
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
|
|
291
|
+
|
|
292
|
+
A model can be simply created with:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
const book = new Model("book", `https://rest.example.net/api/v1/books`);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
However, in many cases more complex APIs require different settings for the various operations.
|
|
299
|
+
|
|
300
|
+
Instead of an url, you can pass options to perform more elaborated Model's initialization.
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
const options = {
|
|
304
|
+
retrieve: {
|
|
305
|
+
method: "get",
|
|
306
|
+
url: "https://rest.example.net/api/v1/books"
|
|
307
|
+
},
|
|
308
|
+
insert: {
|
|
309
|
+
method: "post",
|
|
310
|
+
url: "https://rest.example.net/api/v1/books"
|
|
311
|
+
},
|
|
312
|
+
update: {
|
|
313
|
+
method: "put",
|
|
314
|
+
url: "https://rest.example.net/api/v1/books"
|
|
315
|
+
},
|
|
316
|
+
delete: {
|
|
317
|
+
method: "delete",
|
|
318
|
+
url: "https://rest.example.net/api/v1/books"
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const book = new Model("book", options);
|
|
323
|
+
```
|
|
324
|
+
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
|
+
|
|
326
|
+
For example, if you want to perform inserts and updates with just `PUT`, you can simply do:
|
|
327
|
+
```js
|
|
328
|
+
const options = {
|
|
329
|
+
retrieve: {
|
|
330
|
+
method: "get",
|
|
331
|
+
url: "https://rest.example.net/api/v1/books"
|
|
332
|
+
},
|
|
333
|
+
insert: {
|
|
334
|
+
method: "put" // It will use the same GET url
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const book = new Model("book", options);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Or, even more flexible, you can pass functions and handle yourself the operations. The functions MUST return promises.
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
const options = {
|
|
346
|
+
retrieve: () => {
|
|
347
|
+
// 1) get the data from the API
|
|
348
|
+
// 2) tranforms the data
|
|
349
|
+
// 3) return the data to the store
|
|
350
|
+
return Promise.resolve(data);
|
|
351
|
+
},
|
|
352
|
+
insert: (data) => {
|
|
353
|
+
// 1) recieve the data from the store
|
|
354
|
+
// 2) transform the data however you like
|
|
355
|
+
// 3) send data to server
|
|
356
|
+
return Promise.resolve();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const book = new Model("book", options);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Relations among models
|
|
364
|
+
|
|
365
|
+
Optionally, you can create relations among models.
|
|
366
|
+
|
|
367
|
+
For example, you can declare that an author has one or more objects of type book in the following way:
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
const author = new Model("author", `https://rest.example.net/api/v1/authors`);
|
|
371
|
+
const book = new Model("book", `https://rest.example.net/api/v1/books`);
|
|
372
|
+
|
|
373
|
+
author.addRelation(book, "id", "authorId");
|
|
374
|
+
```
|
|
375
|
+
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
|
+
|
|
377
|
+
Now all the author objects will have the 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
|
+
|
|
379
|
+
For example, imagine you have the `author1` object defined in the examples above (Dante Alighieri):
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
author1.getRelation("book")
|
|
383
|
+
.then(dantesBooks => {
|
|
384
|
+
// Do something with Dante's books
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Or..
|
|
388
|
+
author1.getRelation("book", (book) => book.price < 20)
|
|
389
|
+
.then(cheapDantesBooks => {
|
|
390
|
+
// Do something with Dante's books cheaper than 20
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Other ways to declare relations:
|
|
395
|
+
|
|
396
|
+
* `account.addRelation("user", "userId")`
|
|
397
|
+
|
|
398
|
+
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`.
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
* `author.addRelation("book", filterFunction)`
|
|
402
|
+
|
|
403
|
+
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 based on multiple fields/factors; e.g., a `filterFunction` equals to `(author, book) => author.name == book.authorName && author.surname == book.authorSurname`.
|
package/dist/Model.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports["default"] = void 0;
|
|
7
|
+
|
|
8
|
+
var _modelHooksUtils = require("./modelHooksUtils");
|
|
9
|
+
|
|
10
|
+
var _batchPromises = _interopRequireDefault(require("batch-promises"));
|
|
11
|
+
|
|
12
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
|
|
13
|
+
|
|
14
|
+
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
|
|
15
|
+
|
|
16
|
+
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
17
|
+
|
|
18
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
19
|
+
|
|
20
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
21
|
+
|
|
22
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
|
|
23
|
+
|
|
24
|
+
function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
|
|
25
|
+
|
|
26
|
+
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
27
|
+
|
|
28
|
+
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
|
|
29
|
+
|
|
30
|
+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
31
|
+
|
|
32
|
+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
33
|
+
|
|
34
|
+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
35
|
+
|
|
36
|
+
var Model = /*#__PURE__*/_createClass(function Model(name, options) {
|
|
37
|
+
var _this = this;
|
|
38
|
+
|
|
39
|
+
_classCallCheck(this, Model);
|
|
40
|
+
|
|
41
|
+
_defineProperty(this, "_bulkOperation", function (objects, action) {
|
|
42
|
+
if (_this.__singleItemQuery) {
|
|
43
|
+
return (0, _batchPromises["default"])(_this.__batchSize, objects, action);
|
|
44
|
+
} else {
|
|
45
|
+
return action(objects);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
_defineProperty(this, "getType", function () {
|
|
50
|
+
return _this.__type;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
_defineProperty(this, "_toArray", function (data) {
|
|
54
|
+
if (Array.isArray(data)) {
|
|
55
|
+
return data;
|
|
56
|
+
} else {
|
|
57
|
+
_this.__singleItemQuery = true;
|
|
58
|
+
return [data];
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
_defineProperty(this, "retrieveAll", function () {
|
|
63
|
+
return (0, _modelHooksUtils.executeHook)("retrieve", _this.__retrieveHook, null).then(_this._toArray);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
_defineProperty(this, "insertObjects", function (objects) {
|
|
67
|
+
return _this._bulkOperation(objects, _this._insertObjects);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
_defineProperty(this, "updateObjects", function (objects) {
|
|
71
|
+
return _this._bulkOperation(objects, _this._updateObjects);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
_defineProperty(this, "deleteObjects", function (objects) {
|
|
75
|
+
return _this._bulkOperation(objects, _this._deleteObjects);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
_defineProperty(this, "_insertObjects", function (data) {
|
|
79
|
+
return Promise.resolve(true);
|
|
80
|
+
return (0, _modelHooksUtils.executeHook)("insert", _this.__insertHook, data).then(_this._toArray);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
_defineProperty(this, "_updateObjects", function (data) {
|
|
84
|
+
return Promise.resolve(true);
|
|
85
|
+
return (0, _modelHooksUtils.executeHook)("update", _this.__updateHook, data).then(_this._toArray);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
_defineProperty(this, "_deleteObjects", function (data) {
|
|
89
|
+
return Promise.resolve(true);
|
|
90
|
+
return (0, _modelHooksUtils.executeHook)("update", _this.__deleteHook, data).then(_this._toArray);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
_defineProperty(this, "getStore", function () {
|
|
94
|
+
return _this.store;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
_defineProperty(this, "addRelationByField", function (model, localField) {
|
|
98
|
+
var remoteField = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "id";
|
|
99
|
+
|
|
100
|
+
var filterFunction = function filterFunction(parentObject, child) {
|
|
101
|
+
return parentObject[localField] === child[remoteField];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return _this.addRelationByFilter(model, filterFunction);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
_defineProperty(this, "addRelationByFilter", function (model, filterFunction) {
|
|
108
|
+
var includedType = model.getType();
|
|
109
|
+
_this.__includes[includedType] = filterFunction;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
_defineProperty(this, "addRelation", function (model, param2, param3) {
|
|
113
|
+
if (model) {
|
|
114
|
+
_this.getStore().validateModel(model);
|
|
115
|
+
|
|
116
|
+
if (typeof param2 === "string" && (!param3 || typeof param3 === "string")) {
|
|
117
|
+
// explicit model, from, to
|
|
118
|
+
return _this.addRelationByField(model, param2, param3);
|
|
119
|
+
} else if (!param3 && typeof param2 === "function") {
|
|
120
|
+
// explicit model, filterFunction
|
|
121
|
+
return _this.addRelationByFilter(model, param2);
|
|
122
|
+
} else if (!param2 && !param3) {
|
|
123
|
+
// implicit model, from, to (it uses the type as local key and the id as remote key)
|
|
124
|
+
return _this.addRelationByField(model, model.getType(), "id");
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error("Invalid relation declaration");
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
throw new Error("A relation needs a model");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
_defineProperty(this, "getRelation", function (parentObject, includedType, filterFunction) {
|
|
134
|
+
var filterRelation = _this.__includes[includedType];
|
|
135
|
+
|
|
136
|
+
if (filterRelation) {
|
|
137
|
+
return _this.getStore().find(includedType, function (item) {
|
|
138
|
+
return filterRelation(parentObject, item);
|
|
139
|
+
}).then(function (data) {
|
|
140
|
+
if (filterFunction) {
|
|
141
|
+
return data.filter(filterFunction);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return data;
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
return Promise.reject("The relation doesn't exist");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.__type = name;
|
|
152
|
+
|
|
153
|
+
if (!name || !options) {
|
|
154
|
+
throw new Error("A Model requires at least a name and a hook");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var _ref = _typeof(options) === "object" ? (0, _modelHooksUtils.getHooksFromOptions)(options) : (0, _modelHooksUtils.getHooksFromUrl)(options),
|
|
158
|
+
_ref2 = _slicedToArray(_ref, 4),
|
|
159
|
+
retrieveHook = _ref2[0],
|
|
160
|
+
insertHook = _ref2[1],
|
|
161
|
+
updateHook = _ref2[2],
|
|
162
|
+
deleteHook = _ref2[3];
|
|
163
|
+
|
|
164
|
+
this.__retrieveHook = retrieveHook;
|
|
165
|
+
this.__updateHook = updateHook;
|
|
166
|
+
this.__insertHook = insertHook;
|
|
167
|
+
this.__deleteHook = deleteHook;
|
|
168
|
+
this.__singleItemQuery = false; // By default use arrays
|
|
169
|
+
|
|
170
|
+
this.__batchSize = 4; // For HTTP requests in parallel if your API doesn't support multiple resources
|
|
171
|
+
|
|
172
|
+
this.__includes = {};
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
exports["default"] = Model;
|