Skip to content

Commit

Permalink
feat: Add client and BravoGroup methods for creating, updating, and d…
Browse files Browse the repository at this point in the history
…eleting Groups, with tests.
  • Loading branch information
adam-coster committed Oct 4, 2021
1 parent f1e12e6 commit c2dd9f3
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 0 deletions.
50 changes: 50 additions & 0 deletions src/lib/BravoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,9 @@ export class BravoClient extends FavroClient {

//#region GROUPS

/**
* [📄 See the docs.](https://favro.com/developer/#get-all-groups)
*/
async listGroups() {
const res = (await this.requestWithReturnedEntities(
`groups`,
Expand All @@ -859,6 +862,9 @@ export class BravoClient extends FavroClient {
return await res.getAllEntities();
}

/**
* [📄 See the docs.](https://favro.com/developer/#get-a-group)
*/
async findGroupById(groupId: string) {
const res = (await this.requestWithReturnedEntities(
`groups/${groupId}`,
Expand All @@ -868,6 +874,50 @@ export class BravoClient extends FavroClient {
return await res.getFirstEntity();
}

/**
* [📄 See the docs.](https://favro.com/developer/#create-a-group)
*/
async createGroup(options: Pick<FavroApi.Group.Model, 'name' | 'members'>) {
const res = (await this.requestWithReturnedEntities(
`groups`,
{
method: 'post',
body: options,
},
BravoGroup,
)) as BravoResponseEntities<FavroApi.Group.Model, BravoGroup>;
return await res.getFirstEntity();
}

/**
* [📄 See the docs.](https://favro.com/developer/#update-a-group)
*/
async updateGroupById(groupId: string, options: FavroApi.Group.UpdateBody) {
const res = (await this.requestWithReturnedEntities(
`groups/${groupId}`,
{
method: 'put',
body: options,
},
BravoGroup,
)) as BravoResponseEntities<FavroApi.Group.Model, BravoGroup>;
return await res.getFirstEntity();
}

/**
* [📄 See the docs.](https://favro.com/developer/#delete-a-group)
*/
async deleteGroupById(groupId: string) {
await this.deleteEntity(`groups/${groupId}`);
}

async deleteGroupByName(name: string) {
const group = (await this.listGroups()).find((g) => g.name === name);
if (group) {
await this.deleteGroupById(group.groupId);
}
}

//#endregion

private async deleteEntity(url: string) {
Expand Down
15 changes: 15 additions & 0 deletions src/lib/entities/BravoGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ export class BravoGroup extends BravoEntity<FavroApi.Group.Model> {
return [...this._data.members];
}

/**
* Convenience function for the BravoClient's `updateGroup` method.
* Updates the data *for this BravoGroup instance* and returns itself,
* unlike the client method which returns a new instance.
*/
async update(options: FavroApi.Group.UpdateBody) {
const updated = await this._client.updateGroupById(this.groupId, options);
this._data = updated.toJSON();
return this;
}

async delete() {
return this._client.deleteGroupById(this.groupId);
}

equals(group: BravoGroup) {
return (
this.hasSameConstructor(group) &&
Expand Down
25 changes: 25 additions & 0 deletions src/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import type { BravoUser } from '$/lib/entities/BravoUser.js';
import type { BravoTagDefinition } from '$/lib/entities/BravoTag.js';
import type { FavroApi } from '$types/FavroApi.js';
import { BravoWebhookDefinition } from '$/lib/entities/BravoWebhook.js';
import type { BravoGroup } from '$/types/Bravo.js';

/**
* @remarks A root .env file must be populated with the required
Expand All @@ -62,6 +63,7 @@ const testColumnName = '___BRAVO_TEST_COLUMN';
const testCardName = '___BRAVO_TEST_CARD';
const testTagName = '___BRAVO_TEST_TAG';
const testWebhookName = '___BRAVO_TEST_WEBHOOK';
const testGroupName = '___BRAVO_TEST_GROUP';
const testWebhookUrl =
'https://webhook.site/b287bde2-3f81-4d41-ba78-4c36eacdd472';
let testWebhookSecret: string; // Set later with random characters every run
Expand Down Expand Up @@ -161,6 +163,7 @@ describe('BravoClient', function () {
let testUser: BravoUser;
let testTag: BravoTagDefinition;
let testWebhook: BravoWebhookDefinition;
let testGroup: BravoGroup;

// !!!
// Tests are in a specific order to ensure that dependencies
Expand All @@ -173,6 +176,7 @@ describe('BravoClient', function () {
// (Since names aren't required to be unique, there could be quite a mess!)
// NOTE:
await (await client.findTagDefinitionByName(testTagName))?.delete();
await client.deleteGroupByName(testGroupName);
while (true) {
const collection = await client.findCollectionByName(testCollectionName);
if (!collection) {
Expand Down Expand Up @@ -269,6 +273,27 @@ describe('BravoClient', function () {
expect(me!.email).to.equal(myUserEmail);
});

it('can create a group', async function () {
testGroup = await client.createGroup({
name: testGroupName,
members: [{ userId: testUser.userId, role: 'administrator' }],
});
expect(testGroup).to.exist;
expect(testGroup!.name).to.equal(testGroupName);
});

it('can update a group', async function () {
await testGroup.update({
members: [{ email: testUser.email, delete: true }],
});
expect(testGroup.members.length).to.equal(0);
});

it('can delete a group', async function () {
await client.deleteGroupById(testGroup.groupId);
expect(await client.listGroups()).to.have.length(0);
});

describe('Tags', function () {
it('can create a tag', async function () {
const tag = await client.createTagDefinition({
Expand Down
14 changes: 14 additions & 0 deletions src/types/FavroApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { KeyXOR } from './Utility.js';

/**
* Typings for the data models used by the Favro API, as
* a collection of namespaces per general data type (e.g. Widget, Card)
Expand Down Expand Up @@ -116,6 +118,18 @@ export namespace FavroApi {
members: { userId: string; role: FavroApi.Group.ModelFieldValue.Role }[];
}

/**
* [📄 See the docs.](https://favro.com/developer/#update-a-group)
*/
export interface UpdateBody {
name?: string;
members?: (KeyXOR<{ userId: string }, { email: string }> &
KeyXOR<
{ delete: boolean },
{ role: FavroApi.Group.ModelFieldValue.Role }
>)[];
}

export namespace ModelFieldValue {
export type Role = 'administrator' | 'member';
}
Expand Down
10 changes: 10 additions & 0 deletions src/types/Utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,13 @@ export type Nullish = null | undefined;
export type NotNullish<T> = Exclude<T, Nullish>;
export type NotNull<T> = Exclude<T, null>;
export type Defined<T> = Exclude<T, undefined>;

export type KeyDiff<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };

/**
* Given two records, returns a record that can have
* the keys of EITHER `T` or `U`, but not both.
*/
export type KeyXOR<T, U> = T | U extends object
? (KeyDiff<T, U> & U) | (KeyDiff<U, T> & T)
: T | U;

0 comments on commit c2dd9f3

Please sign in to comment.