repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

commit
917725c
parent
6f1d226
author
Eric Bower
date
2024-02-16 18:39:36 +0000 UTC
json api example
2 files changed,  +269, -0
M docs/posts/endpoints.md
+257, -0
  1@@ -117,3 +117,260 @@ const fetchUsersPoll = api.get(["/users", "poll"], { supervisors: poll() });
  2 The first part of the array is what is used for the router, everything else is
  3 unused. This lets you create as many different variations of calling that
  4 endpoint that you need.
  5+
  6+# Using `ctx.req`
  7+
  8+`ctx.req` is a helper function to merge what currently exists inside
  9+`ctx.request` with new properties. It is gaurenteed to return a valid `Request`
 10+object and performs a deep merge on `ctx.request` and what the user provides to
 11+it.
 12+
 13+```ts
 14+const fetchUsers = api.get("/users", function*(ctx, next) {
 15+  ctx.request = ctx.req({
 16+    url: "/psych",
 17+    headers: {
 18+      "Content-Type": "yoyo",
 19+    },
 20+  });
 21+  yield* next();
 22+}
 23+```
 24+
 25+# Middleware
 26+
 27+Because endpoints use the same powerful middleware system employed by thunks, we
 28+can do quite a lot of automating for API requests -- to the point where an
 29+endpoint doesn't have a custom middleware function at all.
 30+
 31+For example, if you API leverages an API specification like JSON API, then we
 32+can automate response processing.
 33+
 34+Given the following API response:
 35+
 36+```json
 37+{
 38+  "links": {
 39+    "self": "http://example.com/articles",
 40+    "next": "http://example.com/articles?page[offset]=2",
 41+    "last": "http://example.com/articles?page[offset]=10"
 42+  },
 43+  "data": [{
 44+    "type": "articles",
 45+    "id": "1",
 46+    "attributes": {
 47+      "title": "JSON:API paints my bikeshed!"
 48+    },
 49+    "relationships": {
 50+      "author": {
 51+        "links": {
 52+          "self": "http://example.com/articles/1/relationships/author",
 53+          "related": "http://example.com/articles/1/author"
 54+        },
 55+        "data": { "type": "people", "id": "9" }
 56+      },
 57+      "comments": {
 58+        "links": {
 59+          "self": "http://example.com/articles/1/relationships/comments",
 60+          "related": "http://example.com/articles/1/comments"
 61+        },
 62+        "data": [
 63+          { "type": "comments", "id": "5" },
 64+          { "type": "comments", "id": "12" }
 65+        ]
 66+      }
 67+    },
 68+    "links": {
 69+      "self": "http://example.com/articles/1"
 70+    }
 71+  }],
 72+  "included": [{
 73+    "type": "people",
 74+    "id": "9",
 75+    "attributes": {
 76+      "firstName": "Dan",
 77+      "lastName": "Gebhardt",
 78+      "twitter": "dgeb"
 79+    },
 80+    "links": {
 81+      "self": "http://example.com/people/9"
 82+    }
 83+  }, {
 84+    "type": "comments",
 85+    "id": "5",
 86+    "attributes": {
 87+      "body": "First!"
 88+    },
 89+    "relationships": {
 90+      "author": {
 91+        "data": { "type": "people", "id": "2" }
 92+      }
 93+    },
 94+    "links": {
 95+      "self": "http://example.com/comments/5"
 96+    }
 97+  }, {
 98+    "type": "comments",
 99+    "id": "12",
100+    "attributes": {
101+      "body": "I like XML better"
102+    },
103+    "relationships": {
104+      "author": {
105+        "data": { "type": "people", "id": "9" }
106+      }
107+    },
108+    "links": {
109+      "self": "http://example.com/comments/12"
110+    }
111+  }]
112+}
113+```
114+
115+We could create a middleware:
116+
117+```ts
118+import { createApi, mdw } from "starfx";
119+import {
120+  createSchema,
121+  select,
122+  slice,
123+  storeMdw,
124+  StoreUpdater,
125+} from "starfx/store";
126+
127+interface Article {
128+  id: string;
129+  title: string;
130+  authorId: string;
131+  comments: string[];
132+}
133+
134+function deserializeArticle(art: any): Article {
135+  return {
136+    id: art.id,
137+    title: art.attributes.title,
138+    authorId: art.relationships.author.data.id,
139+    comments: art.relationships.comments.data.map((c) => c.id),
140+  };
141+}
142+
143+interface Person {
144+  id: string;
145+  firstName: string;
146+  lastName: string;
147+  twitter: string;
148+}
149+
150+function deserializePerson(per: any): Person {
151+  return {
152+    id: per.id,
153+    firstName: per.attributes.firstName,
154+    lastName: per.attributes.lastName,
155+    twitter: per.attributes.twitter,
156+  };
157+}
158+
159+interface Comment {
160+  id: string;
161+  body: string;
162+  authorId: string;
163+}
164+
165+function deserializeComment(com: any): Comment {
166+  return {
167+    id: comm.id,
168+    body: com.attributes.body,
169+    authorId: com.relationships.author.data.id,
170+  };
171+}
172+
173+const schema = createSchema({
174+  cache: slice.table(),
175+  loaders: slice.loader(),
176+  token: slice.str(),
177+  articles: slice.table<Article>(),
178+  people: slice.table<Person>(),
179+  comments: slice.table<Comment>(),
180+});
181+type WebState = typeof schema.initialState;
182+
183+const api = createApi();
184+api.use(mdw.api());
185+api.use(storeMdw.store(schema));
186+api.use(api.routes());
187+
188+// do some request setup before making fetch call
189+api.use(function* (ctx, next) {
190+  const token = yield* select(schema.token.select);
191+  ctx.request = ctx.req({
192+    headers: {
193+      "Content-Type": "application/vnd.api+json",
194+      "Authorization": `Bearer ${token}`,
195+    },
196+  });
197+
198+  yield* next();
199+});
200+
201+api.use(mdw.fetch({ baseUrl: "https://json-api.com" }));
202+
203+function process(entity: any): StoreUpdater[] {
204+  if (entity.type === "article") {
205+    const article = deserializeArticle(entity);
206+    return [schema.articles.add({ [article.id]: article })];
207+  } else if (entity.type === "people") {
208+    const person = deserializePerson(entity);
209+    return [schema.people.add({ [person.id]: person })];
210+  } else if (entity.type === "comment") {
211+    const comment = deserializeComment(entity);
212+    return [schema.comments.add({ [comment.id]: comment })];
213+  }
214+
215+  return [];
216+}
217+
218+// parse response
219+api.use(function* (ctx, next) {
220+  // wait for fetch response
221+  yield* next();
222+
223+  if (!ctx.json.ok) {
224+    // bail
225+    return;
226+  }
227+
228+  const updaters: StoreUpdater<WebState>[] = [];
229+  const jso = ctx.json.value;
230+
231+  if (Array.isArray(jso.data)) {
232+    jso.data.forEach(
233+      (entity) => updaters.push(...process(entity)),
234+    );
235+  } else {
236+    updaters.push(...process(jso.data));
237+  }
238+
239+  jso.included.forEach(
240+    (entity) => updaters.push(...process(entity)),
241+  );
242+
243+  yield* schema.update(updaters);
244+});
245+```
246+
247+Now when we create the endpoints, we really don't need a mdw function for them
248+because everything is automated higher in the mdw stack:
249+
250+```ts
251+const fetchArticles = api.get("/articles");
252+const fetchArticle = api.get<{ id: string }>("/articles/:id");
253+const fetchCommentsByArticleId = api.get<{ id: string }>(
254+  "/articles/:id/comments",
255+);
256+const fetchComment = api.get<{ id: string }>("/comments/:id");
257+```
258+
259+This is simple it is silly not to nomalize the data because we get a ton of
260+benefits from treating our front-end store like a database. CRUD operations
261+become trivial and app-wide.
M docs/posts/thunks.md
+12, -0
 1@@ -93,6 +93,18 @@ makeItSo("123"); // type error!
 2 makeItSo({ id: "123" }); // nice!
 3 ```
 4 
 5+If you do not provide a type for an endpoint, then the action can be dispatched
 6+without a payload:
 7+
 8+```ts
 9+const makeItSo = api.get("make-it-so", function* (ctx, next) {
10+  console.log(ctx.payload);
11+  yield* next();
12+});
13+
14+makeItSo(); // nice!
15+```
16+
17 # Custom `ctx`
18 
19 End-users are able to provide a custom `ctx` object to their thunks. It must