diff --git a/src/lib/BravoClient.ts b/src/lib/BravoClient.ts index 7b7b13e..87bc79b 100644 --- a/src/lib/BravoClient.ts +++ b/src/lib/BravoClient.ts @@ -28,6 +28,31 @@ import { BravoColumn } from './entities/BravoColumn.js'; import { DataFavroColumn } from '$/types/FavroColumnTypes.js'; import { ArrayMatchFunction } from '$/types/Utility.js'; +/** + * The `BravoClient` class should be singly-instanced for a given + * set of credentials and a target organization. Once the organizationId + * is set (either out of the gate via env var or construct args, or + * by using `client.setOrganizationIdByName` when the org name is known + * but the id is not). + * + * All Favro API fetching and caching is centralized and managed in + * this class. "Entities" returned from Favro API endpoints are wrapped + * in per-type class instances, providing shortcuts to many of the methods + * here. + * + * Entities store their raw data, as originally fetched from Favro, + * available via the `.toJSON()` method (this method is automatically + * used by JSON.stringify(), allowing you to use that general function + * to get the raw data back). Note that the raw data + * **does not necessarily get updated** by Bravo when things are mutated. + * + * @example + * const client = new BravoClient(); + * await client.setOrganizationIdByName('my-org'); + * const newCollection = await client.createCollection('My New Collection'); + * await newCollection.delete(); + * // ^^ shortcut for `await client.deleteCollection(newCollection.collectionId)` + */ export class BravoClient extends FavroClient { //#region Organizations private cache = new BravoClientCache(); @@ -289,7 +314,7 @@ export class BravoClient extends FavroClient { { method: 'get', query: { collectionId } }, BravoWidget, )) as BravoResponseEntities; - this.cache.addWidgets(res, collectionId); + this.cache.setWidgets(res, collectionId); } return this.cache.getWidgets(collectionId)!; } @@ -311,7 +336,7 @@ export class BravoClient extends FavroClient { body: { collectionId, name, - type: options?.type || 'backlog', + type: options?.type || 'board', color: options?.color || 'cyan', }, }, @@ -413,18 +438,21 @@ export class BravoClient extends FavroClient { ); const column = (await res.getFirstEntity()) as BravoColumn; assertBravoClaim(column, `Failed to create widget`); - // TODO: UPDATE CACHE + this.cache.addColumn(widgetCommonId, column); return column; } async listColumns(widgetCommonId: string) { - const res = (await this.requestWithReturnedEntities( - `columns`, - { method: 'get', query: { widgetCommonId } }, - BravoColumn, - )) as BravoResponseEntities; - // TODO: UPDATE CACHE - return await res.getAllEntities(); + if (!this.cache.getColumns(widgetCommonId)) { + const res = (await this.requestWithReturnedEntities( + `columns`, + { method: 'get', query: { widgetCommonId } }, + BravoColumn, + )) as BravoResponseEntities; + const columns = await res.getAllEntities(); + this.cache.setColumns(widgetCommonId, columns); + } + return this.cache.getColumns(widgetCommonId)!; } /** @@ -437,9 +465,11 @@ export class BravoClient extends FavroClient { return await find(await this.listColumns(widgetCommonId), matchFunction); } - async deleteColumnById(columnId: string) { - // TODO: UPDATE CACHE + async deleteColumn(widgetCommonId: string, columnId: string) { + // Note: technically we don't NEED the widgetId to delete a column, + // but coupling these together is useful and allows for cache management. await this.deleteEntity(`columns/${columnId}`); + this.cache.removeColumn(widgetCommonId, columnId); } //#endregion diff --git a/src/lib/clientLib/BravoClientCache.ts b/src/lib/clientLib/BravoClientCache.ts index c0e2bba..a528a14 100644 --- a/src/lib/clientLib/BravoClientCache.ts +++ b/src/lib/clientLib/BravoClientCache.ts @@ -3,6 +3,7 @@ import { BravoUser } from '$entities/users'; import { BravoCollection } from '$entities/BravoCollection.js'; import type { BravoResponseWidgets } from './BravoResponse.js'; import { assertBravoClaim } from '$lib/errors.js'; +import { BravoColumn } from '../entities/BravoColumn.js'; export class BravoClientCache { protected _organizations?: BravoOrganization[]; @@ -13,6 +14,13 @@ export class BravoClientCache { * used to key the paging result from not using a collectionId (global). */ protected _widgets: Map = new Map(); + /** + * Widget columns are fetched separately via the API. They can + * be fetched directly, but more likely will all be fetched at once + * for a given Widget. Caching on a per-widget basis probably + * makes the most sense. + */ + protected _columns: Map = new Map(); get collections() { // @ts-expect-error @@ -53,11 +61,56 @@ export class BravoClientCache { * a given collection. If the collectionId is unset, or `''`, it's * assumed the widget pager is from a */ - addWidgets(widgetPager: BravoResponseWidgets, collectionId = '') { + setWidgets(widgetPager: BravoResponseWidgets, collectionId = '') { assertBravoClaim(widgetPager, 'Must provide a widget pager!'); this._widgets.set(collectionId, widgetPager); } + getColumns(collectionId: string) { + const columns = this._columns.get(collectionId); + return columns && [...columns]; + } + + /** Set the cache for the columns of a widget */ + setColumns(widgetCommonId: string, columns: BravoColumn[]) { + assertBravoClaim(widgetCommonId, 'Must provide a widget id!'); + this._columns.set(widgetCommonId, [...columns]); + } + + /** + * Replace/add a cached column for a widget. + * Useful for updating the cache after adding or + * updating a column. + * Does nothing if there isn't already a cached + * list of columns for this widget. + * + * **Use with caution:** you can create bad caches with this! + */ + addColumn(widgetCommonId: string, column: BravoColumn) { + this.removeColumn(widgetCommonId, column.columnId); + // Only add if the cache already exists + this._columns.get(widgetCommonId)?.push(column); + } + + /** + * Remove a cached column for a widget. + * Useful for updating the cache after deleting a column. + * Does nothing if there isn't already a cached + * list of columns for this widget. + * + * **Use with caution:** you can create bad caches with this! + */ + removeColumn(widgetCommonId: string, columnId: string) { + const columns = this._columns.get(widgetCommonId); + if (!columns) { + return; + } + const idx = columns.findIndex((col) => col.columnId == columnId); + if (idx > -1) { + columns.splice(idx, 1); + } + } + /** * Add a collection to the cache *if the cache already exists*, * e.g. for updating it after creating a new collection. Ensures @@ -103,5 +156,6 @@ export class BravoClientCache { this._organizations = undefined; this._collections = undefined; this._widgets.clear(); + this._columns.clear(); } }