Skip to content

Commit

Permalink
feat: Simplify API for Favro response parsing by using async iterator…
Browse files Browse the repository at this point in the history
…s, and add find methods for Widgets.
  • Loading branch information
adam-coster committed Jul 1, 2021
1 parent d9117d7 commit 111bf17
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 84 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"no-empty": "off",
"no-empty-function": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": "error",
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ As environment variables:
- ✔ Find collection by id
- ✔ Create a collection
- ✔ Delete a collection
- Delete with method on Collection instance
- Delete with method on Collection instance
- ✔ List widgets
- Find widgets by name
- ✔ Find widget by ID
- ✔ Find widgets by name
- Create a widget
- List cards
- Search cards by title
Expand Down
110 changes: 89 additions & 21 deletions src/lib/BravoClient.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { assertBravoClaim } from './errors.js';
import {
OptionFavroCollectionVisibility,
OptionFavroCollectionColorBackground,
OptionFavroCollectionRole,
DataAnyEntity,
ConstructorFavroEntity,
} from '../types/FavroApiTypes';
import { BravoClientCache } from './clientLib/BravoClientCache.js';
import { BravoResponseEntities } from './clientLib/BravoResponse.js';
import { FavroClient, OptionsFavroRequest } from './clientLib/FavroClient.js';
import {
findByField,
findRequiredByField,
Expand All @@ -14,11 +10,15 @@ import {
import { FavroCollection } from './FavroCollection';
import { FavroUser } from './FavroUser';
import { FavroOrganization } from './FavroOrganization';
import { FavroClient, OptionsFavroRequest } from './clientLib/FavroClient.js';
import { BravoClientCache } from './clientLib/BravoClientCache.js';
import { BravoResponse } from './clientLib/BravoResponse.js';
import { FavroWidget } from './FavroWidget.js';
import { DataFavroWidget } from '$/types/FavroWidgetTypes.js';
import type { DataFavroWidget } from '$/types/FavroWidgetTypes.js';
import type {
OptionFavroCollectionVisibility,
OptionFavroCollectionColorBackground,
OptionFavroCollectionRole,
DataAnyEntity,
ConstructorFavroEntity,
} from '$/types/FavroApiTypes';

export class BravoClient extends FavroClient {
//#region Organizations
Expand All @@ -30,7 +30,7 @@ export class BravoClient extends FavroClient {
entityClass: ConstructorFavroEntity<EntityData>,
) {
const res = await this.request(url, options, entityClass);
return new BravoResponse(this, entityClass, res);
return new BravoResponseEntities(this, entityClass, res);
}

async currentOrganization() {
Expand Down Expand Up @@ -182,7 +182,7 @@ export class BravoClient extends FavroClient {
},
FavroCollection,
);
const collection = (await res.getFetchedEntities())[0] as
const collection = (await res.getAllEntities())[0] as
| FavroCollection
| undefined;
assertBravoClaim(collection, `Failed to create collection`);
Expand Down Expand Up @@ -268,7 +268,7 @@ export class BravoClient extends FavroClient {
{ method: 'get' },
FavroCollection,
);
const collections = await res.getFetchedEntities();
const collections = await res.getAllEntities();
assertBravoClaim(
collections.length == 1,
`No collection found with id ${collectionId}`,
Expand All @@ -283,17 +283,85 @@ export class BravoClient extends FavroClient {
//#region Widgets

/**
* Get the Widgets from the organization, with optional filtering.
* The returned object contains only the first page of results, plus
* methods to fetch additional (or all) pages. Results *are not* cached.
* Get the Widgets from the organization as an async generator,
* allowing you to loop over however many results you want without
* having to exhaustively fetch all widgets (can reduce API calls).
*/
private async getWidgetsAsyncGenerator(collectionId?: string) {
const widgets = this.cache.getWidgets(collectionId);
if (!widgets) {
const res = (await this.requestWithReturnedEntities(
'widgets',
{ method: 'get', query: { collectionId } },
FavroWidget,
)) as BravoResponseEntities<DataFavroWidget, FavroWidget>;
this.cache.addWidgets(res, collectionId);
}
return this.cache.getWidgets(collectionId)!;
}

/**
* Get list of all widgets, globally or by collection.
* Specify the collection for faster results and
* fewer API calls. Uses the `.getWidgetsPager()` cache.
*/
async listWidgets(options?: { collectionId?: string; archived?: boolean }) {
async listWidgets(collectionId?: string) {
const pager = await this.getWidgetsAsyncGenerator(collectionId);
const widgets = ((await pager?.getAllEntities()) || []) as FavroWidget[];
return widgets;
}

async findWidgetById(widgetCommonId: string) {
const res = await this.requestWithReturnedEntities(
'widgets',
{ method: 'get', query: options },
`widgets/${widgetCommonId}`,
{ method: 'get' },
FavroWidget,
);
return res as BravoResponse<DataFavroWidget, FavroWidget>;
const [widget] = await res.getAllEntities();
return widget as FavroWidget;
}

/**
* Find the first widget for which the `matchFunction` returns a truthy value.
* Specifying a collection is recommended to reduce API calls. API calls caused
* by the search are cached.
*/
async findWidget(
matchFunction: (widget: FavroWidget, idx?: number) => any,
collectionId = '',
) {
// Reduce API calls by non-exhaustively searching (when possible)
const widgets = await this.getWidgetsAsyncGenerator(collectionId);
return await widgets.find(matchFunction);
}

/**
* Find the first widget found matching a given name.
* Ignores case by default.
*
* Results are cached. Specifying a collectionId is
* recommended to reduce API calls.
*
* *Note that Favro does not
* require unique Widget names: you'll need to ensure
* that yourself or restrict searches to collections
* containing only one widget matching the given name.*
*/
async findWidgetByName(
name: string,
collectionId?: string,
options?: {
matchCase?: boolean;
},
) {
// Reduce API calls by non-exhaustively searching (when possible)
return await this.findWidget(
(widget) =>
options?.matchCase
? widget.name == name
: stringsMatchIgnoringCase(name, widget.name),
collectionId,
);
}

//#endregion
Expand Down
5 changes: 5 additions & 0 deletions src/lib/FavroCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ export class FavroCollection extends FavroEntity<DataFavroCollection> {
get organizationId() {
return this._data.organizationId;
}

/** Delete this collection from Favro. **Use with care!** */
async delete() {
await this._client.deleteCollectionById(this.collectionId);
}
}
31 changes: 30 additions & 1 deletion src/lib/clientLib/BravoClientCache.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { FavroOrganization } from '@/FavroOrganization.js';
import { FavroUser } from '@/FavroUser.js';
import { FavroCollection } from '@/FavroCollection.js';
import type { BravoResponseWidgets } from './BravoResponse.js';
import { assertBravoClaim } from '@/errors.js';

export class BravoClientCache {
protected _organizations?: FavroOrganization[];
protected _users?: FavroUser[];
protected _collections?: FavroCollection[];
/**
* Widget paging results keyed by collectionId, with the empty string `''`
* used to key the paging result from not using a collectionId (global).
*/
protected _widgets: Map<string, BravoResponseWidgets> = new Map();

get collections() {
// @ts-expect-error
Expand All @@ -31,6 +38,26 @@ export class BravoClientCache {
this._organizations = orgs;
}

/**
* Get the widget paging object from a get-all-widgets
* search, keyed by collectionId. If the collectionId
* is not provided, or set to `''`, the global all-widgets
* pager is returned (if in the cache).
*/
getWidgets(collectionId = '') {
return this._widgets.get(collectionId);
}

/**
* Replace the stored widget pager (or add if there isn't one) for
* a given collection. If the collectionId is unset, or `''`, it's
* assumed the widget pager is from a
*/
addWidgets(widgetPager: BravoResponseWidgets, collectionId = '') {
assertBravoClaim(widgetPager, 'Must provide a widget pager!');
this._widgets.set(collectionId, widgetPager);
}

/**
* Add a collection to the cache *if the cache already exists*,
* e.g. for updating it after creating a new collection. Ensures
Expand Down Expand Up @@ -70,9 +97,11 @@ export class BravoClientCache {
* To reduce API calls (the rate limits are tight), things
* are generally cached. To ensure requests are up to date
* with recent changes, you can force a cache clear.
*/ clear() {
*/
clear() {
this._users = undefined;
this._organizations = undefined;
this._collections = undefined;
this._widgets.clear();
}
}
Loading

0 comments on commit 111bf17

Please sign in to comment.