Skip to content

Commit

Permalink
feat: Add Widget deletion methods and get all tests passing.
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-coster committed Jul 2, 2021
1 parent 95c320e commit 1c2898a
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 61 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ As environment variables:
- ✔ Find collection by id
- ✔ Create a collection
- ✔ Delete a collection
- ✔ Delete with method on Collection instance
- ✔ List widgets
- ✔ Find widget by ID
- ✔ Find widgets by name
- Create a widget
- ✔ Create a widget
- ✔ Delete a widget
- List cards
- Search cards by title
- Create a card
Expand Down
41 changes: 23 additions & 18 deletions src/lib/BravoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,14 @@ export class BravoClient extends FavroClient {
}

/**
* Collection names aren't required to be unique. This
* method will delete *all* collections matching the name
* provided. **Warning** case insensitive matching is used!
* Delete the first collection found by matching against
* a name.
*/
async deleteCollectionsByName(name: string) {
const collections = await this.findCollectionsByName(name);
for (const collection of collections) {
await this.deleteCollectionById(collection.collectionId);
}
async deleteCollectionByName(
...args: Parameters<BravoClient['findCollectionByName']>
) {
const collection = await this.findCollectionByName(...args);
return await collection?.delete();
}

/**
Expand All @@ -237,16 +236,12 @@ export class BravoClient extends FavroClient {
}

/**
* Find collections by name. Names are not required to be unique
* for Favro Collections, so *all* matching Collections are returned.
* Find a collection by name. Names are not required to be unique
* by Favro -- only the first match is returned.
* {@link https://favro.com/developer/#get-a-collection}
*/
async findCollectionsByName(name: string) {
const collections = await this.listCollections();
const matching = collections.filter((c) =>
stringsMatchIgnoringCase(name, c.name),
);
return matching;
async findCollectionByName(name: string, options?: { ignoreCase?: boolean }) {
return findByField(await this.listCollections(), 'name', name, options);
}

/**
Expand Down Expand Up @@ -380,19 +375,29 @@ export class BravoClient extends FavroClient {
name: string,
collectionId?: string,
options?: {
matchCase?: boolean;
ignoreCase?: boolean;
},
) {
// Reduce API calls by non-exhaustively searching (when possible)
return await this.findWidget(
(widget) =>
options?.matchCase
!options?.ignoreCase
? widget.name == name
: stringsMatchIgnoringCase(name, widget.name),
collectionId,
);
}

async deleteWidgetById(widgetCommonId: string) {
const res = await this.request(`widgets/${widgetCommonId}`, {
method: 'delete',
});
assertBravoClaim(
res.succeeded,
`Failed to delete collection with status ${res.status}`,
);
}

//#endregion

/**
Expand Down
24 changes: 23 additions & 1 deletion src/lib/BravoEntity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import type { DataAnyEntity } from '$/types/FavroApiTypes.js';
import { BravoClient } from './BravoClient.js';

/**
* Base class for Favro Entities (wrapping raw API data)
*/
export class BravoEntity<EntityData> {
export abstract class BravoEntity<EntityData extends DataAnyEntity> {
protected _deleted = false;

constructor(protected _client: BravoClient, protected _data: EntityData) {}

/**
* If this *specific instance* was used to delete itself.
* This could be `false` despite the source Favro data having
* been deleted in some other way.
*/
get deleted() {
return this._deleted;
}

/**
* Check if another entity represents the same underlying data
* (do not have to be the same instance)
*/
abstract equals(otherEntity: any): boolean;

hasSameConstructor(otherEntity: any) {
return this.constructor === otherEntity.constructor;
}

toJSON() {
return { ...this._data };
}
Expand Down
1 change: 0 additions & 1 deletion src/lib/clientLib/BravoClientCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { BravoUser } from '$entities/users';
import { BravoCollection } from '$entities/BravoCollection.js';
import type { BravoResponseWidgets } from './BravoResponse.js';
import { assertBravoClaim } from '$lib/errors.js';
import { BravoWidget } from '$entities/BravoWidget.js';

export class BravoClientCache {
protected _organizations?: BravoOrganization[];
Expand Down
13 changes: 12 additions & 1 deletion src/lib/entities/BravoCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class BravoCollection extends BravoEntity<DataFavroCollection> {
async findWidgetByName(
name: string,
options?: {
matchCase?: boolean;
ignoreCase?: boolean;
},
) {
return await this._client.findWidgetByName(
Expand All @@ -56,6 +56,17 @@ export class BravoCollection extends BravoEntity<DataFavroCollection> {

/** Delete this collection from Favro. **Use with care!** */
async delete() {
if (this.deleted) {
return;
}
await this._client.deleteCollectionById(this.collectionId);
this._deleted = true;
}

equals(collection: BravoCollection) {
return (
this.hasSameConstructor(collection) &&
this.collectionId === collection.collectionId
);
}
}
6 changes: 6 additions & 0 deletions src/lib/entities/BravoOrganization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ export class BravoOrganization extends BravoEntity<DataFavroOrganization> {
get organizationId() {
return this._data.organizationId;
}

equals(org: BravoOrganization) {
return (
this.hasSameConstructor(org) && this.organizationId === org.organizationId
);
}
}
14 changes: 14 additions & 0 deletions src/lib/entities/BravoWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ export class BravoWidget extends BravoEntity<DataFavroWidget> {
return this._data.editRole;
}

async delete() {
if (!this.deleted) {
await this._client.deleteWidgetById(this.widgetCommonId);
}
this._deleted = true;
}

equals(widget: BravoWidget) {
return (
this.hasSameConstructor(widget) &&
this.widgetCommonId === widget.widgetCommonId
);
}

/** Allowed colors for Widgets */
static get colors() {
return [
Expand Down
5 changes: 5 additions & 0 deletions src/lib/entities/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export class BravoUserBase<
get userId() {
return this._data.userId;
}
equals<User extends BravoUserBase<any>>(otherUser: User) {
return (
this.hasSameConstructor(otherUser) && this.userId === otherUser.userId
);
}
}

export class BravoUser extends BravoUserBase<DataFavroUser> {
Expand Down
81 changes: 47 additions & 34 deletions src/test/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ describe('BravoClient', function () {
// rate limits.

const client = new BravoClient();
let testWidget: BravoWidget;
let testCollection: BravoCollection;

// !!!
// Tests are in a specific order to ensure that dependencies
// happen first, and cleanup happens last. This is tricky to
// do with good test design -- to minimize API calls (the limits
// are low) the tests become dependent on the outcomes of prior tests.

before(function () {
resetSandbox();
Expand Down Expand Up @@ -120,51 +128,54 @@ describe('BravoClient', function () {
});

describe('Collections', function () {
this.bail(true);

let testCollection: BravoCollection;
it('can create a collection', async function () {
testCollection = await client.createCollection(testCollectionName);
assertBravoTestClaim(testCollection, 'Collection not created');
});

it('can list all collections', async function () {
const collections = await client.listCollections();
expect(
collections.length,
'Should have found at least one collection',
).to.be.greaterThan(0);
it('can find created collection', async function () {
const foundCollection = await client.findCollectionByName(
testCollectionName.toLocaleLowerCase(),
{ ignoreCase: true },
);
assertBravoTestClaim(foundCollection, 'Collection not created');
expect(
collections.every((c) => c.collectionId),
'Collections should have collectionIds',
foundCollection.equals(testCollection),
'Found and created collections should match',
).to.be.true;
});

it('can create a collection', async function () {
testCollection = await client.createCollection(testCollectionName);
assertBravoTestClaim(testCollection, 'Collection not created');
it('can create a widget', async function () {
testWidget = await testCollection.createWidget(testWidgetName, {
color: 'cyan',
});
expect(testWidget, 'Should be able to create widget').to.exist;
});

// Put Widgets inside so that the heirarchical aspects are easier to test
// without spamming the Favro API.
describe('Widgets (a.k.a. "Boards")', function () {
this.bail(true);
let testWidget: BravoWidget;

it('can create a widget', async function () {
testWidget = await testCollection.createWidget(testWidgetName, {
color: 'cyan',
});
assertBravoTestClaim(testWidget);
});
it('can find created widget', async function () {
// Grab the first widget found
const widget = await testCollection.findWidgetByName(
testWidgetName.toLocaleLowerCase(),
{ ignoreCase: true },
);
assertBravoTestClaim(widget, 'Should be able to fetch created widget');
expect(
widget.equals(testWidget),
'Found widget should match created widget',
).to.be.true;
});

it('can fetch widgets', async function () {
// Grab the first widget found
const widget = await testCollection.findWidgetByName(
testWidgetName.toLocaleLowerCase(),
);
// TODO: NEXT HEIRARCHY LEVEL

assertBravoTestClaim(widget, 'Should be able to fetch a single widget');
});
it('can delete a created widget', async function () {
await testWidget.delete();
await expectAsyncError(
() => client.findWidgetById(testWidget.widgetCommonId),
'Should not find deleted widget',
);
});

it('can delete a collection', async function () {
it('can delete a created collection', async function () {
await testCollection.delete();
await expectAsyncError(
() => client.findCollectionById(testCollection.collectionId),
Expand All @@ -176,4 +187,6 @@ describe('BravoClient', function () {
after(function () {
resetSandbox();
});

after(function () {});
});
8 changes: 4 additions & 4 deletions src/types/FavroApiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ export interface DataFavroResponsePaged<DataEntity extends DataAnyEntity> {
requestId: string;
entities: DataEntity[];
}
export type DataFavroResponse<DataEntity extends DataAnyEntity> =
| DataFavroResponsePaged<DataEntity>
| DataEntity;
export type DataFavroResponse<EntityData extends DataAnyEntity> =
| DataFavroResponsePaged<EntityData>
| EntityData;

export type ConstructorFavroEntity<EntityData> = new (
export type ConstructorFavroEntity<EntityData extends DataAnyEntity> = new (
client: BravoClient,
data: EntityData,
) => BravoEntity<EntityData>;

0 comments on commit 1c2898a

Please sign in to comment.