@superbia/untrue 1.1.4
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/LICENSE +21 -0
- package/README.md +524 -0
- package/index.js +3 -0
- package/package.json +21 -0
- package/src/DocumentContext.js +63 -0
- package/src/RequestContext.js +155 -0
- package/src/RequestWrapper.js +39 -0
- package/src/SuperbiaContext.js +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 iconshot
|
|
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,524 @@
|
|
|
1
|
+
# @superbia/untrue
|
|
2
|
+
|
|
3
|
+
Integrate Superbia and Untrue.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i @superbia/untrue
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Get started
|
|
12
|
+
|
|
13
|
+
We will use the Untrue's Context API to integrate Superbia and Untrue. Two contexts are needed:
|
|
14
|
+
|
|
15
|
+
- `DocumentContext`: It will store all the documents returned by the Superbia requests and subscriptions, e.g., users, posts, comments, etc.
|
|
16
|
+
- `RequestContext` It will store all the requests we do via a Superbia client.
|
|
17
|
+
|
|
18
|
+
#### What is a Document?
|
|
19
|
+
|
|
20
|
+
A document is an object that has an ID and a typename.
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"user": {
|
|
25
|
+
"_typename": "User",
|
|
26
|
+
"_id": "123",
|
|
27
|
+
"name": "Jhon Doe"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
In this case the ID is found at `_id`. This may change from app to app according to how your data has been structured. Common keys for ID are: `id`, `ID`, `_id`.
|
|
33
|
+
|
|
34
|
+
`_typename`, on the other hand, is automatically added by Superbia in the server side.
|
|
35
|
+
|
|
36
|
+
## DocumentContext
|
|
37
|
+
|
|
38
|
+
`DocumentContext` will intercept any data handled by the `client` and it will group the documents based on their ID and typename.
|
|
39
|
+
|
|
40
|
+
If we receive some client data like:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"user": {
|
|
45
|
+
"_typename": "User",
|
|
46
|
+
"_id": "123",
|
|
47
|
+
"name": "Jhon Doe",
|
|
48
|
+
"username": "jhondoe",
|
|
49
|
+
"lastPost": {
|
|
50
|
+
"_typename": "Post",
|
|
51
|
+
"_id": "456",
|
|
52
|
+
"text": "Hello world"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
... the `DocumentContext` data will be:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
this.documents = {
|
|
62
|
+
User: {
|
|
63
|
+
123: {
|
|
64
|
+
_typename: "User",
|
|
65
|
+
_id: "123",
|
|
66
|
+
name: "Jhon Doe",
|
|
67
|
+
username: "jhondoe",
|
|
68
|
+
lastPost: "456",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
Post: {
|
|
72
|
+
456: {
|
|
73
|
+
_typename: "Post",
|
|
74
|
+
_id: "456",
|
|
75
|
+
text: "Hello world",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Notice how for `lastPost` we only store the `id`. We do this to have a single source of truth for every document.
|
|
82
|
+
|
|
83
|
+
### Creation
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
import { DocumentContext } from "@superbia/untrue";
|
|
87
|
+
|
|
88
|
+
import { client } from "./client";
|
|
89
|
+
|
|
90
|
+
class AppDocumentContext extends DocumentContext {
|
|
91
|
+
onFollow = (userId) => {
|
|
92
|
+
const user = this.documents.User[userId]; // get the user
|
|
93
|
+
|
|
94
|
+
// update user
|
|
95
|
+
|
|
96
|
+
user.following = true;
|
|
97
|
+
user.followersCount++;
|
|
98
|
+
|
|
99
|
+
this.update(); // notify the Wrapper of changes
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
onUnfollow = (userId) => {
|
|
103
|
+
const user = this.documents.User[userId]; // get the user
|
|
104
|
+
|
|
105
|
+
// update user
|
|
106
|
+
|
|
107
|
+
user.following = false;
|
|
108
|
+
user.followersCount--;
|
|
109
|
+
|
|
110
|
+
this.update(); // notify the Wrapper of changes
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default new AppDocumentContext(client, {
|
|
115
|
+
id: "_id",
|
|
116
|
+
typename: "_typename",
|
|
117
|
+
}); // the client and the keys of documents
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Usage
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
import { Node, Wrapper } from "untrue";
|
|
124
|
+
|
|
125
|
+
import AppDocumentContext from "./AppDocumentContext";
|
|
126
|
+
|
|
127
|
+
function User({ userId, name, username, following, onFollow, onUnfollow }) {
|
|
128
|
+
const onFollowUser = () => onFollow(userId);
|
|
129
|
+
const onUnfollowUser = () => onUnfollow(userId);
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
new Node("span", name),
|
|
133
|
+
new Node("span", `@${username}`),
|
|
134
|
+
new Node(
|
|
135
|
+
"button",
|
|
136
|
+
{
|
|
137
|
+
onclick: following ? onUnfollowUser : onFollowUser,
|
|
138
|
+
},
|
|
139
|
+
following ? "unfollow" : "follow"
|
|
140
|
+
),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default Wrapper.wrapContext(User, AppDocumentContext, (props) => {
|
|
145
|
+
const { userId } = props;
|
|
146
|
+
|
|
147
|
+
const documents = AppDocumentContext.getDocuments(); // all the documents
|
|
148
|
+
|
|
149
|
+
const { name, username, following } = documents.User[userId]; // the desired user document
|
|
150
|
+
|
|
151
|
+
const { onFollow, onUnfollow } = AppDocumentContext; // context handlers
|
|
152
|
+
|
|
153
|
+
return { name, username, following, onFollow, onUnfollow }; // data the component needs
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## RequestContext
|
|
158
|
+
|
|
159
|
+
`RequestContext` will keep the state of the requests.
|
|
160
|
+
|
|
161
|
+
Every `request` has 4 properties:
|
|
162
|
+
|
|
163
|
+
- `loading`: `Boolean`. `true` if the request is loading.
|
|
164
|
+
- `done`: `Boolean`. `true` if the request has been completed and it has no error.
|
|
165
|
+
- `data`: `Object`. Result of the request. `null` if the request hasn't been completed yet or an error was found.
|
|
166
|
+
- `error`: `Error` object or `null`. It's an `Error` object if the request has been completed but there's an error in the request itself or in any endpoint.
|
|
167
|
+
|
|
168
|
+
The next example assumes we have an endpoint `userPosts` that returns an array of `Post` documents.
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"userPosts": [
|
|
173
|
+
{ "_typename": "Post", "_id": "123", "text": "Hello world" },
|
|
174
|
+
{ "_typename": "Post", "_id": "456", "text": "Lorem ipsum" }
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Just as in `DocumentContext`, we won't store the entire documents but their IDs only.
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
this.requests = {
|
|
183
|
+
someRequestKey: {
|
|
184
|
+
loading: false,
|
|
185
|
+
done: true,
|
|
186
|
+
error: null,
|
|
187
|
+
data: { userPosts: ["123", "456"] },
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Creation
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { RequestContext } from "@superbia/untrue";
|
|
196
|
+
|
|
197
|
+
import { client } from "./client";
|
|
198
|
+
|
|
199
|
+
class AppRequestContext extends RequestContext {
|
|
200
|
+
onSomeHandler = () => {
|
|
201
|
+
// we can update this.requests data directly from here
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default new AppRequestContext(client, {
|
|
206
|
+
id: "_id",
|
|
207
|
+
typename: "_typename",
|
|
208
|
+
}); // the client and the keys of documents
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Usage
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
import { Component, Node, Wrapper } from "untrue";
|
|
215
|
+
|
|
216
|
+
import { RequestWrapper } from "@superbia/untrue";
|
|
217
|
+
|
|
218
|
+
import Post from "./Post";
|
|
219
|
+
|
|
220
|
+
class PostList extends Component {
|
|
221
|
+
constructor(props) {
|
|
222
|
+
super(props);
|
|
223
|
+
|
|
224
|
+
this.on("mount", this.handleMountRequest); // request on mount
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
handleMountRequest = () => {
|
|
228
|
+
const { requestKey, userId, onRequest } = this.props;
|
|
229
|
+
|
|
230
|
+
// it will be requested as:
|
|
231
|
+
// client.request({ userPosts: { userId } })
|
|
232
|
+
|
|
233
|
+
onRequest(requestKey, { userPosts: { userId } });
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
render() {
|
|
237
|
+
const { loading, done, error, postIds } = this.props;
|
|
238
|
+
|
|
239
|
+
if (loading) {
|
|
240
|
+
return new Node("span", "Loading...");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (error !== null) {
|
|
244
|
+
return new Node("span", error.message);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (done) {
|
|
248
|
+
return postIds.map((postId) => new Node(Post, { postId }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// RequestWrapper will add the `requestKey` prop
|
|
256
|
+
|
|
257
|
+
export default RequestWrapper.wrapRequester(
|
|
258
|
+
Wrapper.wrapContext(PostList, AppRequestContext, (props) => {
|
|
259
|
+
const { requestKey, userId } = props;
|
|
260
|
+
|
|
261
|
+
const requests = AppRequestContext.getRequests(); // all the requests
|
|
262
|
+
|
|
263
|
+
// the desired request, it's undefined until onRequest is fired
|
|
264
|
+
|
|
265
|
+
const {
|
|
266
|
+
loading = false,
|
|
267
|
+
done = false,
|
|
268
|
+
error = null,
|
|
269
|
+
data = null,
|
|
270
|
+
} = requests?.[requestKey] ?? {};
|
|
271
|
+
|
|
272
|
+
const postIds = data !== null ? data.userPosts : [];
|
|
273
|
+
|
|
274
|
+
const { onRequest } = AppRequestContext; // context handler
|
|
275
|
+
|
|
276
|
+
return { loading, done, error, postIds, onRequest }; // data the component needs
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Intercepting requests
|
|
282
|
+
|
|
283
|
+
As we know, documents are stored in `DocumentContext` and requests are stored in `RequestContext`.
|
|
284
|
+
|
|
285
|
+
Let's say you want to update a document when a specific endpoint is requested.
|
|
286
|
+
|
|
287
|
+
You can `intercept` a request to do so. You need to override the `intercept` method to return an object of interceptors.
|
|
288
|
+
|
|
289
|
+
`AppDocumentContext.js`:
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
class AppDocumentContext extends DocumentContext {
|
|
293
|
+
onLike = (postId) => {
|
|
294
|
+
const post = this.documents.Post[postId];
|
|
295
|
+
|
|
296
|
+
post.liked = true;
|
|
297
|
+
post.likesCount++;
|
|
298
|
+
|
|
299
|
+
this.update();
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`AppRequestContext.js`:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
import AppDocumentContext from "./AppDocumentContext";
|
|
308
|
+
|
|
309
|
+
class AppRequestContext extends RequestContext {
|
|
310
|
+
intercept() {
|
|
311
|
+
return {
|
|
312
|
+
likePost: {
|
|
313
|
+
load: (requestKey, endpoints) => {
|
|
314
|
+
// every time `likePost` is requested, this closure will be executed
|
|
315
|
+
|
|
316
|
+
const { postId } = endpoints.likePost;
|
|
317
|
+
|
|
318
|
+
AppDocumentContext.onLike(postId);
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Then in the Component, we call:
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
const { postId } = this.props;
|
|
330
|
+
|
|
331
|
+
onRequest(null, { likePost: { postId } });
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Paginated requests
|
|
335
|
+
|
|
336
|
+
To implement paginated requests the server must return a Pagination object.
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"userPosts": {
|
|
341
|
+
"_typename": "PostPagination",
|
|
342
|
+
"nodes": [
|
|
343
|
+
{ "_typename": "Post", "_id": "123", "text": "Hello world" },
|
|
344
|
+
{ "_typename": "Post", "_id": "456", "text": "Lorem ipsum" }
|
|
345
|
+
],
|
|
346
|
+
"pageInfo": {
|
|
347
|
+
"_typename": "PaginationPageInfo",
|
|
348
|
+
"hasNextPage": true,
|
|
349
|
+
"nextPageCursor": "789"
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`RequestContext` will manage to store the data like:
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
this.requests = {
|
|
359
|
+
someRequestKey: {
|
|
360
|
+
loading: false,
|
|
361
|
+
done: false,
|
|
362
|
+
error: null,
|
|
363
|
+
data: {
|
|
364
|
+
userPosts: {
|
|
365
|
+
loading: false,
|
|
366
|
+
error: null,
|
|
367
|
+
data: {
|
|
368
|
+
nodes: ["123", "456"],
|
|
369
|
+
pageInfo: { hasNextPage: true, nextPageCursor: "789" },
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Notice how we have two set of properties for `loading`, `error` and `data`. The first set will belong to the `onRequest` call while the second one will belong to the `onLoad` calls.
|
|
378
|
+
|
|
379
|
+
The rules of documents will be applied here, so every `node` will be stored as an ID only.
|
|
380
|
+
|
|
381
|
+
#### Usage
|
|
382
|
+
|
|
383
|
+
We will need two different components: one for the initial `onRequest` call and another one for the subsequent `onLoad` calls.
|
|
384
|
+
|
|
385
|
+
`PostList.js`
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
import { Component, Node, Wrapper } from "untrue";
|
|
389
|
+
|
|
390
|
+
import { RequestWrapper } from "@superbia/untrue";
|
|
391
|
+
|
|
392
|
+
import AppRequestContext from "./AppRequestContext";
|
|
393
|
+
|
|
394
|
+
import Content from "./Content";
|
|
395
|
+
|
|
396
|
+
class PostList extends Component {
|
|
397
|
+
constructor(props) {
|
|
398
|
+
super(props);
|
|
399
|
+
|
|
400
|
+
this.on("mount", this.handleMountRequest); // request on mount
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
handleMountRequest = () => {
|
|
404
|
+
const { requestKey, userId, onRequest } = this.props;
|
|
405
|
+
|
|
406
|
+
onRequest(requestKey, { userPosts: { userId, limit: 20 } });
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
render() {
|
|
410
|
+
const { requestKey, userId, loading, done, error } = this.props;
|
|
411
|
+
|
|
412
|
+
if (loading) {
|
|
413
|
+
return new Node("span", "Loading...");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (error !== null) {
|
|
417
|
+
return new Node("span", error.message);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (done) {
|
|
421
|
+
return new Node(Content, { requestKey, userId }); // pass requestKey and userId
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export default RequestWrapper.wrapRequester(
|
|
429
|
+
Wrapper.wrapContext(PostList, AppRequestContext, (props) => {
|
|
430
|
+
const { requestKey } = props;
|
|
431
|
+
|
|
432
|
+
const requests = AppRequestContext.getRequests(); // all the requests
|
|
433
|
+
|
|
434
|
+
const {
|
|
435
|
+
loading = false,
|
|
436
|
+
done = false,
|
|
437
|
+
error = null,
|
|
438
|
+
} = requests?.[requestKey] ?? {}; // the desire request
|
|
439
|
+
|
|
440
|
+
const { onRequest } = AppRequestContext; // context handler
|
|
441
|
+
|
|
442
|
+
return { loading, done, error, onRequest }; // data the component needs
|
|
443
|
+
})
|
|
444
|
+
);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
`Content.js`
|
|
448
|
+
|
|
449
|
+
```js
|
|
450
|
+
import { Node, Wrapper } from "untrue";
|
|
451
|
+
|
|
452
|
+
import AppRequestContext from "./AppRequestContext";
|
|
453
|
+
|
|
454
|
+
import Post from "./Post";
|
|
455
|
+
|
|
456
|
+
function Content({
|
|
457
|
+
requestKey,
|
|
458
|
+
userId,
|
|
459
|
+
loading,
|
|
460
|
+
error,
|
|
461
|
+
postIds,
|
|
462
|
+
hasNextPage,
|
|
463
|
+
nextPageCursor,
|
|
464
|
+
onLoad,
|
|
465
|
+
}) {
|
|
466
|
+
const onLoadNext = () => {
|
|
467
|
+
onLoad(requestKey, {
|
|
468
|
+
userPosts: { userId, limit: 20, cursor: nextPageCursor },
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
return [
|
|
473
|
+
postIds.map((postId) => new Node(Post, { postId })),
|
|
474
|
+
hasNextPage
|
|
475
|
+
? new Node("button", { onclick: onLoadNext }, "load next page")
|
|
476
|
+
: null,
|
|
477
|
+
loading ? new Node("span", "Loading next page...") : null,
|
|
478
|
+
error !== null ? new Node("span", error.message) : null,
|
|
479
|
+
];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// here we don't need RequestWrapper because we receive the "requestKey" as a prop already
|
|
483
|
+
|
|
484
|
+
export default Wrapper.wrapContext(Content, AppRequestContext, (props) => {
|
|
485
|
+
const { requestKey } = props;
|
|
486
|
+
|
|
487
|
+
const requests = AppRequestContext.getRequests(); // all the requests
|
|
488
|
+
|
|
489
|
+
const {
|
|
490
|
+
loading,
|
|
491
|
+
error,
|
|
492
|
+
data: {
|
|
493
|
+
nodes: postIds, // renaming for convenience
|
|
494
|
+
pageInfo: { hasNextPage, nextPageCursor },
|
|
495
|
+
},
|
|
496
|
+
} = requests[requestKey].data.userPosts; // the desired request's data
|
|
497
|
+
|
|
498
|
+
const { onLoad } = AppRequestContext; // context handler
|
|
499
|
+
|
|
500
|
+
return { loading, error, postIds, hasNextPage, nextPageCursor, onLoad }; // data the component needs
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## RequestWrapper
|
|
505
|
+
|
|
506
|
+
`RequestWrapper` exposes a method `wrapRequester`. This method will add a `requestKey` prop to the component.
|
|
507
|
+
|
|
508
|
+
```js
|
|
509
|
+
import { RequestWrapper } from "@superbia/untrue";
|
|
510
|
+
|
|
511
|
+
function Child({ requestKey }) {
|
|
512
|
+
// ...
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const UsernameRequester = RequestWrapper.wrapRequester(Child, (props) => {
|
|
516
|
+
const { username } = props;
|
|
517
|
+
|
|
518
|
+
return username;
|
|
519
|
+
}); // requestKey will be the username prop
|
|
520
|
+
|
|
521
|
+
const ValueRequester = RequestWrapper.wrapRequester(Child, "profile"); // requestKey will be "profile"
|
|
522
|
+
|
|
523
|
+
const UniqueRequester = RequestWrapper.wrapRequester(Child); // requestKey will be a unique id
|
|
524
|
+
```
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@superbia/untrue",
|
|
3
|
+
"version": "1.1.4",
|
|
4
|
+
"description": "Integrate Superbia and Untrue.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/iconshot/superbia-untrue.git"
|
|
8
|
+
},
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"superbia",
|
|
12
|
+
"untrue"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"untrue": "^3.11.8"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"uuid": "^9.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import SuperbiaContext from "./SuperbiaContext";
|
|
2
|
+
|
|
3
|
+
export class DocumentContext extends SuperbiaContext {
|
|
4
|
+
constructor(client, documentKeys) {
|
|
5
|
+
super(documentKeys);
|
|
6
|
+
|
|
7
|
+
this.documents = {};
|
|
8
|
+
|
|
9
|
+
client
|
|
10
|
+
.on("request", (endpoints, emitter) => {
|
|
11
|
+
emitter.on("data", (data) => {
|
|
12
|
+
this.onData(data);
|
|
13
|
+
});
|
|
14
|
+
})
|
|
15
|
+
.on("subscribe", (endpoint, emitter) => {
|
|
16
|
+
emitter.on("data", (data) => {
|
|
17
|
+
this.onData(data);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// default persistence
|
|
23
|
+
|
|
24
|
+
hydrate(documents) {
|
|
25
|
+
this.documents = documents;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
persist() {
|
|
29
|
+
return this.documents;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getDocuments() {
|
|
33
|
+
return this.documents;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onData(data) {
|
|
37
|
+
const newData = {};
|
|
38
|
+
|
|
39
|
+
Object.values(data).forEach((result) => {
|
|
40
|
+
this.parseResult(result, newData);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const types = Object.keys(newData);
|
|
44
|
+
|
|
45
|
+
if (types.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const type of types) {
|
|
50
|
+
if (!(type in this.documents)) {
|
|
51
|
+
this.documents[type] = {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const newDocuments = newData[type];
|
|
55
|
+
|
|
56
|
+
for (const id in newDocuments) {
|
|
57
|
+
this.documents[type][id] = newDocuments[id];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.update();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import SuperbiaContext from "./SuperbiaContext";
|
|
2
|
+
|
|
3
|
+
export class RequestContext extends SuperbiaContext {
|
|
4
|
+
constructor(client, documentKeys) {
|
|
5
|
+
super(documentKeys);
|
|
6
|
+
|
|
7
|
+
this.requests = {};
|
|
8
|
+
|
|
9
|
+
this.client = client;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// default persistence
|
|
13
|
+
|
|
14
|
+
hydrate(requests) {
|
|
15
|
+
this.requests = requests;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
persist() {
|
|
19
|
+
return this.requests;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getRequests() {
|
|
23
|
+
return this.requests;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
intercept() {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
parseRequestResult(result) {
|
|
31
|
+
if (
|
|
32
|
+
result !== null &&
|
|
33
|
+
typeof result === "object" &&
|
|
34
|
+
this.documentKeys.typename in result &&
|
|
35
|
+
result[this.documentKeys.typename].endsWith("Pagination")
|
|
36
|
+
) {
|
|
37
|
+
return { loading: false, error: null, data: this.parseResult(result) };
|
|
38
|
+
} else {
|
|
39
|
+
return this.parseResult(result);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onRequest = async (key, endpoints, payload = null) => {
|
|
44
|
+
key = key !== null && key !== undefined ? key : Date.now().toString();
|
|
45
|
+
|
|
46
|
+
this.requests[key] = {
|
|
47
|
+
loading: true,
|
|
48
|
+
done: false,
|
|
49
|
+
error: null,
|
|
50
|
+
data: null,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const interceptors = this.intercept();
|
|
54
|
+
|
|
55
|
+
const endpointKeys = Object.keys(endpoints);
|
|
56
|
+
|
|
57
|
+
for (const endpointKey of endpointKeys) {
|
|
58
|
+
if (endpointKey in interceptors && "load" in interceptors[endpointKey]) {
|
|
59
|
+
interceptors[endpointKey].load(key, endpoints, payload);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.update();
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await this.client.request(endpoints);
|
|
67
|
+
|
|
68
|
+
const data = response.data();
|
|
69
|
+
|
|
70
|
+
const newData = {};
|
|
71
|
+
|
|
72
|
+
for (const key in data) {
|
|
73
|
+
newData[key] = this.parseRequestResult(data[key]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.requests[key] = {
|
|
77
|
+
loading: false,
|
|
78
|
+
done: true,
|
|
79
|
+
error: null,
|
|
80
|
+
data: newData,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
for (const endpointKey of endpointKeys) {
|
|
84
|
+
if (
|
|
85
|
+
endpointKey in interceptors &&
|
|
86
|
+
"data" in interceptors[endpointKey]
|
|
87
|
+
) {
|
|
88
|
+
interceptors[endpointKey].data(key, endpoints, payload, data);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.update();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
this.requests[key] = { loading: false, done: false, error, data: null };
|
|
95
|
+
|
|
96
|
+
for (const endpointKey of endpointKeys) {
|
|
97
|
+
if (
|
|
98
|
+
endpointKey in interceptors &&
|
|
99
|
+
"error" in interceptors[endpointKey]
|
|
100
|
+
) {
|
|
101
|
+
interceptors[endpointKey].error(key, endpoints, payload, error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.update();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
onLoad = async (key, endpoints, payload = null) => {
|
|
110
|
+
const interceptors = this.intercept();
|
|
111
|
+
|
|
112
|
+
const endpointKey = Object.keys(endpoints)[0];
|
|
113
|
+
|
|
114
|
+
const result = this.requests[key].data[endpointKey];
|
|
115
|
+
|
|
116
|
+
result.loading = true;
|
|
117
|
+
result.error = null;
|
|
118
|
+
|
|
119
|
+
if (endpointKey in interceptors && "load" in interceptors[endpointKey]) {
|
|
120
|
+
interceptors[endpointKey].load(key, endpoints, payload);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.update();
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const response = await this.client.request(endpoints);
|
|
127
|
+
|
|
128
|
+
const data = response.data();
|
|
129
|
+
|
|
130
|
+
const endpointResult = this.parseResult(data[endpointKey]);
|
|
131
|
+
|
|
132
|
+
result.loading = false;
|
|
133
|
+
|
|
134
|
+
result.data = {
|
|
135
|
+
...endpointResult,
|
|
136
|
+
nodes: [...result.data.nodes, ...endpointResult.nodes],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (endpointKey in interceptors && "data" in interceptors[endpointKey]) {
|
|
140
|
+
interceptors[endpointKey].data(key, endpoints, payload, data);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.update();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
result.loading = false;
|
|
146
|
+
result.error = error;
|
|
147
|
+
|
|
148
|
+
if (endpointKey in interceptors && "error" in interceptors[endpointKey]) {
|
|
149
|
+
interceptors[endpointKey].error(key, endpoints, payload, error);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.update();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Component, Node } from "untrue";
|
|
2
|
+
|
|
3
|
+
import { v4 as uuid } from "uuid";
|
|
4
|
+
|
|
5
|
+
export class RequestWrapper {
|
|
6
|
+
static wrapRequester(Child, requestKeyExtractor = null) {
|
|
7
|
+
return class RequestComponent extends Component {
|
|
8
|
+
constructor(props) {
|
|
9
|
+
super(props);
|
|
10
|
+
|
|
11
|
+
let key = null;
|
|
12
|
+
|
|
13
|
+
if (requestKeyExtractor !== null) {
|
|
14
|
+
if (typeof requestKeyExtractor === "function") {
|
|
15
|
+
key = requestKeyExtractor(props);
|
|
16
|
+
} else {
|
|
17
|
+
key = requestKeyExtractor;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (key === null) {
|
|
22
|
+
key = uuid();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.requestKey = key;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
const { children, ...props } = this.props;
|
|
30
|
+
|
|
31
|
+
return new Node(
|
|
32
|
+
Child,
|
|
33
|
+
{ ...props, requestKey: this.requestKey },
|
|
34
|
+
children
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Context } from "untrue";
|
|
2
|
+
|
|
3
|
+
class SuperbiaContext extends Context {
|
|
4
|
+
constructor(documentKeys) {
|
|
5
|
+
super();
|
|
6
|
+
|
|
7
|
+
this.documentKeys = documentKeys;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
parseResult(result, data = {}) {
|
|
11
|
+
if (result === null) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(result)) {
|
|
16
|
+
return result.map((item) => this.parseResult(item, data));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof result === "object") {
|
|
20
|
+
const newResult = {};
|
|
21
|
+
|
|
22
|
+
for (const key in result) {
|
|
23
|
+
newResult[key] = this.parseResult(result[key], data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isDocument =
|
|
27
|
+
this.documentKeys.id in result && this.documentKeys.typename in result;
|
|
28
|
+
|
|
29
|
+
if (!isDocument) {
|
|
30
|
+
return newResult;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const id = result[this.documentKeys.id];
|
|
34
|
+
const typename = result[this.documentKeys.typename];
|
|
35
|
+
|
|
36
|
+
if (!(typename in data)) {
|
|
37
|
+
data[typename] = {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
data[typename][id] = newResult;
|
|
41
|
+
|
|
42
|
+
return result[this.documentKeys.id];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default SuperbiaContext;
|