Skip to content

Commit

Permalink
feat: Create a distinct BravoResponse class that can page and hydrate…
Browse files Browse the repository at this point in the history
… results.
  • Loading branch information
adam-coster committed Jun 27, 2021
1 parent 0a575ac commit 4ebf40a
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 156 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ As environment variables:

## TODO

- ✔ Client class
- ✔ Client classes
- ✔ Separate request methods for those that
return hydratable entities and those that
do not.
- ✔ Method for paging & hydrating search results
- ✔ List orgs
- ✔ Find org by name
- ✔ Set org by name or ID
Expand All @@ -38,13 +42,14 @@ As environment variables:
- ✔ Find collection by id
- ✔ Create a collection
- ✔ Delete a collection
- Delete with method on Collection instance
- List widgets
- Find widgets by name
- Create a widget
- Pass class constructors to API requests to simplify boilerplate
- Create a base class
- Have the first constructor arg be the BravoClient instance
- Auto-page fetched results
- List cards
- Search cards by title
- Create a card
- Delete a card

## Usage

Expand Down
48 changes: 30 additions & 18 deletions src/lib/BravoClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { assertBravoClaim } from './errors.js';
import {
DataFavroUser,
DataFavroCollection,
OptionFavroCollectionVisibility,
OptionFavroCollectionColorBackground,
OptionFavroCollectionRole,
DataAnyEntity,
ConstructorFavroEntity,
} from '../types/FavroApiTypes';
import {
findByField,
Expand All @@ -14,13 +14,23 @@ import {
import { FavroCollection } from './FavroCollection';
import { FavroUser } from './FavroUser';
import { FavroOrganization } from './FavroOrganization';
import { FavroClient } from './clientLib/FavroClient.js';
import { FavroClient, OptionsFavroRequest } from './clientLib/FavroClient.js';
import { BravoClientCache } from './clientLib/BravoClientCache.js';
import { BravoResponse } from './clientLib/BravoResponse.js';

export class BravoClient extends FavroClient {
//#region Organizations
private cache = new BravoClientCache();

private async requestWithReturnedEntities<EntityData extends DataAnyEntity>(
url: string,
options: OptionsFavroRequest,
entityClass: ConstructorFavroEntity<EntityData>,
) {
const res = await this.request(url, options, entityClass);
return new BravoResponse(this, entityClass, res);
}

async currentOrganization() {
if (!this._organizationId) {
return;
Expand All @@ -37,14 +47,15 @@ export class BravoClient extends FavroClient {
*/
async listOrganizations() {
if (!this.cache.organizations) {
const res = await this.request(
const res = await this.requestWithReturnedEntities(
'organizations',
{
excludeOrganizationId: true,
},
FavroOrganization,
);
this.cache.organizations = res.entities as FavroOrganization[];
this.cache.organizations =
(await res.getAllEntities()) as FavroOrganization[];
}
return this.cache.organizations;
}
Expand Down Expand Up @@ -85,12 +96,12 @@ export class BravoClient extends FavroClient {
const org = await this.currentOrganization();
assertBravoClaim(org, 'Organization not set');
if (!this.cache.users) {
const res = await this.request<DataFavroUser>(
const res = await this.requestWithReturnedEntities(
'users',
{ method: 'get' },
FavroUser,
);
this.cache.users = res.entities as FavroUser[];
this.cache.users = (await res.getAllEntities()) as FavroUser[];
}
return this.cache.users;
}
Expand Down Expand Up @@ -153,7 +164,7 @@ export class BravoClient extends FavroClient {
}[];
},
) {
const res = await this.request(
const res = await this.requestWithReturnedEntities(
'collections',
{
method: 'post',
Expand All @@ -169,11 +180,10 @@ export class BravoClient extends FavroClient {
},
FavroCollection,
);
const collection = res.entities[0] as FavroCollection | undefined;
assertBravoClaim(
collection,
`Failed to create collection with status: ${res.status}`,
);
const collection = (await res.getFetchedEntities())[0] as
| FavroCollection
| undefined;
assertBravoClaim(collection, `Failed to create collection`);
this.cache.addCollection(collection);
return collection;
}
Expand Down Expand Up @@ -212,12 +222,13 @@ export class BravoClient extends FavroClient {
const org = await this.currentOrganization();
assertBravoClaim(org, 'Organization not set');
if (!this.cache.collections) {
const res = await this.request(
const res = await this.requestWithReturnedEntities(
'collections',
{ method: 'get' },
FavroCollection,
);
this.cache.collections = res.entities as FavroCollection[];
this.cache.collections =
(await res.getAllEntities()) as FavroCollection[];
}
return this.cache.collections;
}
Expand Down Expand Up @@ -250,16 +261,17 @@ export class BravoClient extends FavroClient {
);
if (!collection) {
// Then hit the API directly!
const res = await this.request<DataFavroCollection>(
const res = await this.requestWithReturnedEntities(
`collections/${collectionId}`,
{ method: 'get' },
FavroCollection,
);
const collections = await res.getFetchedEntities();
assertBravoClaim(
res.entities.length == 1,
collections.length == 1,
`No collection found with id ${collectionId}`,
);
collection = res.entities[0] as FavroCollection;
collection = collections[0] as FavroCollection;
}
return collection;
}
Expand Down
52 changes: 0 additions & 52 deletions src/lib/FavroResponse.ts

This file was deleted.

69 changes: 69 additions & 0 deletions src/lib/clientLib/BravoResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { BravoClient } from '../BravoClient';
import { FavroEntity } from '../FavroEntity';
import { FavroResponse } from './FavroResponse';

/**
* Pager for hydrated Favro API responses
*/
export class BravoResponse<EntityData, Entity extends FavroEntity<EntityData>> {
private _entities: Entity[] = [];
private _hydratedLatestPage = false;

constructor(
private _client: BravoClient,
private EntityClass: new (client: BravoClient, data: EntityData) => Entity,
private _latestPage?: FavroResponse<EntityData>,
) {}

/**
* For paged responses, fetch, hydrate, and add the next page of
* results to the cache. If there is not a next page, returns `undefined`.
* If there is, returns *only* the hydrated entities for that page.
* Useful for rate-limit-conscious searching.
*/
async fetchNextPage() {
// Ensure the prior page got added!
await this.addLatestPageToCache();
if (!this._latestPage || (await this._latestPage.isLastPage())) {
return;
}
this._latestPage = await this._latestPage.getNextPageResponse();
this._hydratedLatestPage = false;
const newPage = await this.addLatestPageToCache();
return newPage;
}

/**
* Exhaustively page all results and return all entities.
* Uses the cache.
*/
async getAllEntities() {
while (await this.fetchNextPage()) {}
return this.getFetchedEntities();
}

/**
* Get entities paged and fetched so far, hydrated as entity class instances (based
* on what was provided to the constructor) */
async getFetchedEntities() {
// Ensure everything is hydrated
await this.addLatestPageToCache();
return [...this._entities];
}

/**
* Ensure hydration of the latest page of results and append
* them to the cache. If the latest page is already hydrated, returns nothing.
* Else returns a copy of *just that page* of hydrated entities.
*/
private async addLatestPageToCache() {
if (!this._hydratedLatestPage && this._latestPage) {
const newEntities = (await this._latestPage.getEntitiesData()).map(
(e) => new this.EntityClass(this._client, e),
);
this._entities.push(...newEntities);
this._hydratedLatestPage = true;
return newEntities;
}
}
}
Loading

0 comments on commit 4ebf40a

Please sign in to comment.