HydratingAPIProviderclass
new HydratingAPIProvider<P, R>(source: APIProvider<P, R>, db: MemoryDBProvider)
| Param | Type | |
|---|---|---|
source | APIProvider<P, R> | Abstract base for API providers that send requests to a set of Endpoint definitions rooted at a common base URL. required |
db | MemoryDBProvider | Synchronous in-memory database provider, storing each collection in a MemoryTable. required |
| Return | |
|---|---|
HydratingAPIProvider<P, R> | API provider that validates every result, then writes the collection items in it into a local database. |
| Property | Type | |
|---|---|---|
.db | MemoryDBProvider | The local database the items are written into. required readonly |
API provider that validates every result, then writes the collection items in it into a local database.
- Only results that use a collection's own schemas hydrate:
Collection.item,Collection.itemsandCollection.queryItems, or aDataSchemawhose props use them (one level deep). Every other result passes through with no change. Collection.itemsets the item. Anullorundefinedresult (e.g.NULLABLE(USERS.item)) deletes the item with theidfrom the payload (the payload itself, or itsidprop).Collection.itemssets each item. It never deletes items.Collection.queryItemssets each item, then marks the query as loaded (seeMemoryTable.setQueryTime()).- Writes happen synchronously before
call()resolves, so no store sees half of a result. - Extends
ValidationAPIProvider, so it only ever writes validated items.
A wrapping provider that validates every result, then writes the collection items in it into a local MemoryDBProvider. One API response can then fill the stores for several collections, and useItem() / useQuery() show the items at once.
Things to know
- Only results that use a collection's own schemas hydrate:
Collection.item,Collection.itemsandCollection.queryItems, or aDataSchemawhose props use them (one level deep). Every other result passes through with no change. ARRAY(USERS.item)does not hydrate, because it does not point at the collection. UseUSERS.items.- A
nullorundefinedresult forNULLABLE(USERS.item)deletes the item with theidfrom the payload. The table then knows the item is missing, so anItemStoreshows it as missing and not as loading. Collection.itemsonly adds and replaces items. It never deletes them, so an item the server dropped stays until something deletes it.Collection.queryItemsalso marks its query as loaded (seeMemoryTable.setQueryTime()), so aQueryStorefor exactly that query shows its items, or shows that it has none, without loading. The query matches byJSON.stringify(), so build it in one shared function for the server and the client.- Do not trust the
queryin aCollection.queryItemsresult. It is only checked to be an object. - Writes happen synchronously before
call()resolves, so no store sees half of a result. - It extends
ValidationAPIProvider, so it only writes validated items.
Usage
import { ClientAPIProvider, GET, HydratingAPIProvider } from "shelving/api";
import { COLLECTION, MemoryDBProvider } from "shelving/db";
import { DATA, NULLABLE, STRING } from "shelving/schema";
const SQUADS = COLLECTION("squads", STRING, { title: STRING });
const MEMBERS = COLLECTION("members", STRING, { squad: STRING, user: STRING });
// One response returns two collections.
const MY_SQUADS = GET("/squad/me", DATA({}), DATA({ squads: SQUADS.items, members: MEMBERS.queryItems }));
const GET_SQUAD = GET("/squad/{id}", DATA({ id: STRING }), NULLABLE(SQUADS.item));
const DB = new MemoryDBProvider();
const API = new HydratingAPIProvider(new ClientAPIProvider({ url: "https://api.example.com" }), DB);
await API.call(MY_SQUADS, {}); // Writes the squads and members into `DB`, and marks the members query as loaded.
await API.call(GET_SQUAD, { id: "reds" }); // Sets the squad, or deletes it if the result is `null`.The server sends the query with the items, so the client knows which query the items answer:
// shared/query.ts — one function builds the query on the server and the client.
export const getMyMembersQuery = (user: string): Query<MemberItem> => ({ user, $order: "squad" });
// Server handler.
MY_SQUADS.handler(async (_payload, _request, { user }) => ({
squads: await getMySquads(user),
members: { query: getMyMembersQuery(user), items: await getMyMembers(user) },
}));Paging
Page an endpoint with getQueryItemsAfter(). It takes the cursor from the last page, never from the cache:
const page = await API.call(LIST_MESSAGES, { squad, $order: "!when", $limit: 30 });
const next = getQueryItemsAfter(page.messages); // `undefined` when there are no more.
if (next) await API.call(LIST_MESSAGES, next);