Skip to content

Commit

Permalink
feat: Add Tag management methods to the Client and improve related me…
Browse files Browse the repository at this point in the history
…thods on Cards. BREAKING
  • Loading branch information
adam-coster committed Aug 28, 2021
1 parent f5c944a commit 4f364e5
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@typescript-eslint/no-floating-promises": ["error"],
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-use-before-define": "off",
Expand Down
97 changes: 92 additions & 5 deletions src/lib/BravoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import type {
} from '$/types/FavroApiTypes';
import type { OptionsBravoCreateWidget } from '$/types/ParameterOptions.js';
import { BravoColumn } from './entities/BravoColumn.js';
import { DataFavroColumn } from '$/types/FavroColumnTypes.js';
import { ArrayMatchFunction } from '$/types/Utility.js';
import type { DataFavroColumn } from '$/types/FavroColumnTypes.js';
import type { ArrayMatchFunction, RequiredBy } from '$/types/Utility.js';
import type {
DataFavroCard,
DataFavroCardAttachment,
Expand All @@ -36,11 +36,13 @@ import type {
} from '$/types/FavroCardTypes.js';
import { BravoCardInstance } from './entities/BravoCard.js';
import { BravoCustomFieldDefinition } from './entities/BravoCustomField.js';
import { DataFavroCustomFieldDefinition } from '$/types/FavroCustomFieldTypes.js';
import { FavroApiParamsCardUpdate } from '$/types/FavroCardUpdateTypes.js';
import type { DataFavroCustomFieldDefinition } from '$/types/FavroCustomFieldTypes.js';
import type { FavroApiParamsCardUpdate } from '$/types/FavroCardUpdateTypes.js';
import type { FavroResponse } from './clientLib/FavroResponse.js';
import { readFileSync } from 'fs';
import { basename } from 'path';
import { BravoTagDefinition } from './entities/BravoTag.js';
import type { FavroDataTypes } from '$/types/FavroTagTypes.js';

/**
* The `BravoClient` class should be singly-instanced for a given
Expand Down Expand Up @@ -160,7 +162,9 @@ export class BravoClient extends FavroClient {
field: 'email' | 'name' | 'userId',
value: string | RegExp,
) {
return await this.findMember(createIsMatchFilter(value, field));
const user = await this.findMember(createIsMatchFilter(value, field));
assertBravoClaim(user, `No user found with ${field} matching ${value}`);
return user;
}

//#endregion
Expand Down Expand Up @@ -639,6 +643,89 @@ export class BravoClient extends FavroClient {

//#endregion

//#region TAGS

/**
* Get and cache *all* tags. Lazy-loads and caches to reduce API calls.
*
* {@link https://favro.com/developer/#get-all-tags}
*/
async listTagDefinitions() {
if (!this.cache.tags) {
const res = (await this.requestWithReturnedEntities(
`tags`,
{ method: 'get' },
BravoTagDefinition,
)) as BravoResponseEntities<
FavroDataTypes.Tag.Definition,
BravoTagDefinition
>;
this.cache.tags = res;
}
return this.cache.tags!;
}

async createTagDefinition(
options: Partial<
Omit<FavroDataTypes.Tag.Definition, 'tagId' | 'organizationId'>
>,
) {
const res = (await this.requestWithReturnedEntities(
`tags`,
{
method: 'post',
body: options,
},
BravoTagDefinition,
)) as BravoResponseEntities<
FavroDataTypes.Tag.Definition,
BravoTagDefinition
>;
return await res.getFirstEntity();
}

async updateTagDefinition(
options: RequiredBy<
Partial<Omit<FavroDataTypes.Tag.Definition, 'organizationId'>>,
'tagId'
>,
) {
const res = (await this.requestWithReturnedEntities(
`tags`,
{
method: 'put',
body: options,
},
BravoTagDefinition,
)) as BravoResponseEntities<
FavroDataTypes.Tag.Definition,
BravoTagDefinition
>;
return await res.getFirstEntity();
}

async findTagDefinitionById(tagId: string) {
const tags = await this.listTagDefinitions();
const tag = await tags.findById('tagId', tagId);
assertBravoClaim(tag, `No tag found with id ${tagId}`);
return tag;
}

async findTagDefinitionByName(name: string | RegExp) {
const tags = await this.listTagDefinitions();
return await tags.find(createIsMatchFilter(name, 'name'));
}

async deleteTagById(tagId: string) {
await this.deleteEntity(`tags/${tagId}`);
// The caching method makes it hard to invalidate a single
// tag entry (since it's all lazy-loaded), so for now do it
// the DUMB WAY: clear the entire tags cache.
this.cache.tags = undefined;
}

//#endregion

//#region CUSTOM FIELDS

/**
Expand Down
15 changes: 15 additions & 0 deletions src/lib/clientLib/BravoClientCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { BravoUser } from '$/lib/entities/BravoUser';
import type { BravoCollection } from '$entities/BravoCollection.js';
import type {
BravoResponseCustomFields,
BravoResponseTags,
BravoResponseWidgets,
} from './BravoResponse.js';
import type { BravoColumn } from '../entities/BravoColumn.js';
Expand Down Expand Up @@ -34,6 +35,12 @@ export class BravoClientCache {
*/
protected _customFields?: BravoResponseCustomFields;

/**
* Tags are handled like Custom Fields, since they are global
* and not meaningfully searchable via the API.
*/
protected _tags?: BravoResponseTags;

get collections() {
// @ts-expect-error
return this._collections ? [...this._collections] : undefined;
Expand Down Expand Up @@ -66,6 +73,13 @@ export class BravoClientCache {
this._customFields = customFields;
}

get tags() {
return this._tags;
}
set tags(tags: BravoResponseTags | undefined) {
this._tags = tags;
}

/**
* Get the widget paging object from a get-all-widgets
* search, keyed by collectionId. If the collectionId
Expand Down Expand Up @@ -178,5 +192,6 @@ export class BravoClientCache {
this._widgets.clear();
this._columns.clear();
this._customFields = undefined;
this._tags = undefined;
}
}
11 changes: 9 additions & 2 deletions src/lib/clientLib/BravoResponse.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { BravoClient } from '$lib/BravoClient';
import { BravoEntity } from '$lib/BravoEntity';
import type { BravoClient } from '$lib/BravoClient';
import type { BravoEntity } from '$lib/BravoEntity';
import type { FavroResponse } from './FavroResponse';
import type { DataFavroCustomFieldDefinition } from '$/types/FavroCustomFieldTypes.js';
import type { DataFavroWidget } from '$/types/FavroWidgetTypes.js';
import type { BravoWidget } from '$entities/BravoWidget.js';
import type { BravoCustomFieldDefinition } from '../entities/BravoCustomField.js';
import type { FavroDataTypes } from '$/types/FavroTagTypes.js';
import type { BravoTagDefinition } from '../entities/BravoTag.js';

export type BravoResponseWidgets = BravoResponseEntities<
DataFavroWidget,
Expand All @@ -16,6 +18,11 @@ export type BravoResponseCustomFields = BravoResponseEntities<
BravoCustomFieldDefinition<any>
>;

export type BravoResponseTags = BravoResponseEntities<
FavroDataTypes.Tag.Definition,
BravoTagDefinition
>;

export type BravoResponseEntitiesMatchFunction<Entity> = (
entity: Entity,
idx?: number,
Expand Down
14 changes: 5 additions & 9 deletions src/lib/entities/BravoCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
DataFavroCustomFieldType,
} from '$/types/FavroCardTypes.js';
import type { FavroApiParamsCardUpdate } from '$/types/FavroCardUpdateTypes.js';
import { ExtractKeysByValue } from '$/types/Utility.js';
import type { ExtractKeysByValue } from '$/types/Utility.js';
import { BravoEntity } from '$lib/BravoEntity.js';
import { assertBravoClaim } from '../errors.js';
import { isMatch } from '../utility.js';
Expand Down Expand Up @@ -184,16 +184,12 @@ export class BravoCardInstance extends BravoEntity<DataFavroCard> {
return await this.updateField('removeTagsByName', ...args);
}

async addTagsById(
...args: Parameters<BravoCardUpdateBuilder['addTagsById']>
) {
return await this.updateField('addTagsById', ...args);
async addTags(...args: Parameters<BravoCardUpdateBuilder['addTags']>) {
return await this.updateField('addTags', ...args);
}

async removeTagsById(
...args: Parameters<BravoCardUpdateBuilder['removeTagsById']>
) {
return await this.updateField('removeTagsById', ...args);
async removeTags(...args: Parameters<BravoCardUpdateBuilder['removeTags']>) {
return await this.updateField('removeTags', ...args);
}

async setStartDate(
Expand Down
23 changes: 13 additions & 10 deletions src/lib/entities/BravoCardUpdateBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import {
import type {
DataFavroCardFavroAttachment,
DataFavroCustomFieldType,
DataFavroRating,
} from '$/types/FavroCardTypes.js';
import {
import type {
FavroApiParamsCardCustomField,
FavroApiParamsCardUpdate,
FavroApiParamsCardUpdateArrayField,
FavroApiParamsCardUpdateCustomField,
} from '$/types/FavroCardUpdateTypes.js';
import { DataFavroCustomFieldDefinition } from '$/types/FavroCustomFieldTypes.js';
import { RequiredBy } from '$/types/Utility.js';
import type { DataFavroCustomFieldDefinition } from '$/types/FavroCustomFieldTypes.js';
import type { RequiredBy } from '$/types/Utility.js';
import { assertBravoClaim } from '../errors.js';
import {
addToUniqueArrayBy,
Expand All @@ -22,11 +22,12 @@ import {
stringsOrObjectsToStrings,
wrapIfNotArray,
} from '../utility.js';
import {
import type {
BravoCustomField,
BravoCustomFieldDefinition,
} from './BravoCustomField.js';
import { BravoUser } from './BravoUser.js';
import type { BravoTagDefinition } from './BravoTag.js';
import type { BravoUser } from './BravoUser.js';

export type CustomFieldOrId<FieldType extends DataFavroCustomFieldType = any> =
| string
Expand Down Expand Up @@ -95,12 +96,14 @@ export class BravoCardUpdateBuilder {
return this.addToUniqueArray('removeTags', names, 'addTags');
}

addTagsById(ids: string[]) {
return this.addToUniqueArray('addTagIds', ids, 'removeTagIds');
addTags(tagDefinitionsOrIds: (string | BravoTagDefinition)[]) {
const tagIds = stringsOrObjectsToStrings(tagDefinitionsOrIds, 'tagId');
return this.addToUniqueArray('addTagIds', tagIds, 'removeTagIds');
}

removeTagsById(ids: string[]) {
return this.addToUniqueArray('removeTagIds', ids, 'addTagIds');
removeTags(tagDefinitionsOrIds: (string | BravoTagDefinition)[]) {
const tagIds = stringsOrObjectsToStrings(tagDefinitionsOrIds, 'tagId');
return this.addToUniqueArray('removeTagIds', tagIds, 'addTagIds');
}

setStartDate(date: Date | null) {
Expand Down
29 changes: 29 additions & 0 deletions src/lib/entities/BravoTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { FavroDataTypes } from '$/types/FavroTagTypes.js';
import { BravoEntity } from '$lib/BravoEntity.js';

/** Hydrated Favro Organization. */
export class BravoTagDefinition extends BravoEntity<FavroDataTypes.Tag.Definition> {
get name() {
return this._data.name;
}

get tagId() {
return this._data.tagId;
}

get color() {
return this._data.color;
}

get organizationId() {
return this._data.organizationId;
}

async delete() {
await this._client.deleteTagById(this.tagId);
}

equals(tag: BravoTagDefinition) {
return this.hasSameConstructor(tag) && this.tagId === tag.tagId;
}
}
35 changes: 34 additions & 1 deletion src/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import type { DataFavroCustomFieldType } from '$/types/FavroCardTypes.js';
import { assertBravoClaim } from '$/lib/errors.js';
import type { BravoUser } from '$/lib/entities/BravoUser.js';
import type { BravoTagDefinition } from '$/lib/entities/BravoTag.js';

/**
* @note A root .env file must be populated with the required
Expand All @@ -58,6 +59,7 @@ const testCollectionName = '___BRAVO_TEST_COLLECTION';
const testWidgetName = '___BRAVO_TEST_WIDGET';
const testColumnName = '___BRAVO_TEST_COLUMN';
const testCardName = '___BRAVO_TEST_CARD';
const testTagName = '___BRAVO_TEST_TAG';
const customFieldUniqueName = `Unique Text Field`;
const customFieldRepeatedName = `Repeated Text Field`;
const customFieldUniquenessTestType = 'Text';
Expand Down Expand Up @@ -173,6 +175,7 @@ describe('BravoClient', function () {
let testColumn: BravoColumn;
let testCard: BravoCardInstance;
let testUser: BravoUser;
let testTag: BravoTagDefinition;

// !!!
// Tests are in a specific order to ensure that dependencies
Expand All @@ -185,6 +188,7 @@ describe('BravoClient', function () {
// Clean up any leftover remote testing content
// (Since names aren't required to be unique, there could be quite a mess!)
// NOTE:
await (await client.findTagDefinitionByName(testTagName))?.delete();
while (true) {
const collection = await client.findCollectionByName(testCollectionName);
if (!collection) {
Expand Down Expand Up @@ -245,7 +249,36 @@ describe('BravoClient', function () {
expect(me!.email).to.equal(myUserEmail);
});

describe('Collections', function () {
describe('Tags', function () {
it('can create a tag', async function () {
const tag = await client.createTagDefinition({
name: testTagName,
color: 'purple',
});
expect(tag.name).to.equal(testTagName);
expect(tag.color).to.equal('purple');
testTag = tag;
});

it('can find tags', async function () {
const tags = await client.listTagDefinitions();
const matchingTestTag = await tags.find(
(tag) => tag.name === testTagName,
);
assertBravoClaim(matchingTestTag, 'Tag not found');
expect(
testTag.equals(matchingTestTag),
'Should find created tag with exhaustive search by name',
).to.be.true;

expect(
testTag.equals((await tags.findById('tagId', testTag.tagId))!),
'Should be able to find tag using find-by-id cache',
).to.be.true;
});
});

xdescribe('Collections', function () {
it('can create a collection', async function () {
testCollection = await client.createCollection(testCollectionName);
assertBravoTestClaim(testCollection, 'Collection not created');
Expand Down
Loading

0 comments on commit 4f364e5

Please sign in to comment.