Skip to content

Commit

Permalink
feat: Have all API data wrapped in a class extending a base 'FavroEnt…
Browse files Browse the repository at this point in the history
…ity' class, with access to the client.
  • Loading branch information
adam-coster committed Jun 26, 2021
1 parent c69391b commit 546cc98
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 37 deletions.
36 changes: 21 additions & 15 deletions src/lib/BravoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { assertBravoClaim, BravoError } from './errors.js';
import fetch from 'node-fetch';
import { URL } from 'url';
import {
AnyEntity,
FavroResponseData,
AnyEntityData,
FavroApiMethod,
FavroDataOrganization,
FavroDataOrganizationUser,
FavroDataCollection,
FavroEntityConstructor,
} from '../types/FavroApi';
import { FavroResponse } from './FavroResponse';
import { findByField, findRequiredByField } from './utility.js';
import { FavroCollection } from './FavroCollection';
import { FavroUser } from './FavroUser';
import { FavroOrganization } from './FavroOrganization';

export class BravoClient {
static readonly baseUrl = 'https://favro.com/api/v1';
Expand All @@ -37,7 +37,7 @@ export class BravoClient {
*/
private _backendId?: string;

private _organizations?: FavroDataOrganization[];
private _organizations?: FavroOrganization[];
private _users?: FavroUser<FavroDataOrganizationUser>[];
private _collections?: FavroCollection[];

Expand Down Expand Up @@ -87,8 +87,9 @@ export class BravoClient {
*
* @param url Relative to the base URL {@link https://favro.com/api/v1}
*/
async request<Entity extends AnyEntity = AnyEntity>(
async request<EntityData extends AnyEntityData>(
url: string,
entityClass: FavroEntityConstructor<EntityData>,
options?: {
method?: FavroApiMethod | Capitalize<FavroApiMethod>;
query?: Record<string, string>;
Expand Down Expand Up @@ -153,8 +154,10 @@ export class BravoClient {
body: options?.body,
});
const favroRes = new FavroResponse(
this,
entityClass,
res,
(await res.json()) as FavroResponseData<Entity>,
await res.json(),
);
this._limitResetsAt = favroRes.limitResetsAt;
this._requestsRemaining = favroRes.requestsRemaining;
Expand Down Expand Up @@ -183,10 +186,10 @@ export class BravoClient {
*/
async listOrganizations() {
if (!this._organizations) {
const res = await this.request<FavroDataOrganization>('organizations', {
const res = await this.request('organizations', FavroOrganization, {
excludeOrganizationId: true,
});
this._organizations = res.entities;
this._organizations = res.entities as FavroOrganization[];
}
return [...this._organizations];
}
Expand Down Expand Up @@ -228,8 +231,11 @@ export class BravoClient {
const org = await this.currentOrganization();
assertBravoClaim(org, 'Organization not set');
if (!this._users) {
const res = await this.request<FavroDataOrganizationUser>('users');
this._users = res.entities.map((u) => new FavroUser(u));
const res = await this.request<FavroDataOrganizationUser>(
'users',
FavroUser,
);
this._users = res.entities as FavroUser<FavroDataOrganizationUser>[];
}
return [...this._users];
}
Expand All @@ -241,8 +247,7 @@ export class BravoClient {
async listPartialUsers() {
const org = await this.currentOrganization();
assertBravoClaim(org, 'Organization not set');
const users = org.sharedToUsers.map((u) => new FavroUser(u));
return users;
return org.sharedToUsers;
}

async findFullUserByName(name: string) {
Expand Down Expand Up @@ -273,8 +278,8 @@ export class BravoClient {
const org = await this.currentOrganization();
assertBravoClaim(org, 'Organization not set');
if (!this._collections) {
const res = await this.request<FavroDataCollection>('users');
this._collections = res.entities.map((u) => new FavroCollection(u));
const res = await this.request('users', FavroCollection);
this._collections = res.entities as FavroCollection[];
}
return [...this._collections];
}
Expand All @@ -296,12 +301,13 @@ export class BravoClient {
// Then hit the API directly!
const res = await this.request<FavroDataCollection>(
`collections/${collectionId}`,
FavroCollection,
);
assertBravoClaim(
res.entities.length == 1,
`No collection found with id ${collectionId}`,
);
collection = new FavroCollection(res.entities[0]);
collection = res.entities[0] as FavroCollection;
}
return collection;
}
Expand Down
8 changes: 2 additions & 6 deletions src/lib/FavroCollection.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { FavroDataCollection } from '../types/FavroApi';
import { FavroEntity } from './FavroEntity.js';

export class FavroCollection {
private _data: FavroDataCollection;
constructor(data: FavroDataCollection) {
this._data = data;
}

export class FavroCollection extends FavroEntity<FavroDataCollection> {
get name() {
return this._data.name;
}
Expand Down
12 changes: 12 additions & 0 deletions src/lib/FavroEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BravoClient } from './BravoClient.js';

/**
* Base class for Favro Entities (wrapping raw API data)
*/
export class FavroEntity<EntityData> {
constructor(protected _client: BravoClient, protected _data: EntityData) {}

toJSON() {
return { ...this._data };
}
}
17 changes: 17 additions & 0 deletions src/lib/FavroOrganization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { FavroDataOrganization } from '../types/FavroApi';
import { FavroEntity } from './FavroEntity.js';
import { FavroUser } from './FavroUser.js';

export class FavroOrganization extends FavroEntity<FavroDataOrganization> {
get name() {
return this._data.name;
}

get sharedToUsers() {
return this._data.sharedToUsers.map((u) => new FavroUser(this._client, u));
}

get organizationId() {
return this._data.organizationId;
}
}
16 changes: 12 additions & 4 deletions src/lib/FavroResponse.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { Response } from 'node-fetch';
import { AnyEntity, FavroResponseData } from '../types/FavroApi';
import { FavroResponseData } from '../types/FavroApi';
import type { BravoClient } from './BravoClient.js';
import { FavroEntity } from './FavroEntity';

export class FavroResponse<Entity extends AnyEntity = AnyEntity> {
export class FavroResponse<EntityData, Entity extends FavroEntity<EntityData>> {
private _response: Response;
private _entities: Entity[];

constructor(response: Response, data: FavroResponseData<Entity>) {
constructor(
private _client: BravoClient,
entityClass: new (client: BravoClient, data: EntityData) => Entity,
response: Response,
data: FavroResponseData<EntityData>,
) {
this._response = response;
this._entities = 'entities' in data ? data.entities : [data];
const entitiesData = 'entities' in data ? data.entities : [data];
this._entities = entitiesData.map((e) => new entityClass(_client, e));
// Could be a PAGED response (with an entities field) or not!
// Normalize to always have the data be an array
}
Expand Down
8 changes: 2 additions & 6 deletions src/lib/FavroUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ import {
FavroDataOrganizationUser,
FavroDataOrganizationUserPartial,
} from '../types/FavroApi';
import { FavroEntity } from './FavroEntity.js';

export class FavroUser<
Data extends FavroDataOrganizationUser | FavroDataOrganizationUserPartial,
> {
private _data: Data;
constructor(data: Data) {
this._data = data;
}

> extends FavroEntity<Data> {
get userId() {
return this._data.userId;
}
Expand Down
21 changes: 15 additions & 6 deletions src/types/FavroApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { BravoClient } from '@/BravoClient.js';
import type { FavroEntity } from '@/FavroEntity.js';

export type FavroApiMethod = 'get' | 'post' | 'put' | 'delete';
export type FavroRole =
| 'administrator'
Expand Down Expand Up @@ -57,14 +60,20 @@ export interface FavroDataOrganizationUser {
email: string;
organizationRole: FavroRole;
}
export type AnyEntity = Record<string, any>;
interface FavroResponsePaged<Entity extends AnyEntity = AnyEntity> {
export type AnyEntityData = Record<string, any>;

interface FavroResponsePaged<EntityData extends AnyEntityData> {
limit: number;
page: number;
pages: number;
requestId: string;
entities: Entity[];
entities: EntityData[];
}
export type FavroResponseData<Entity extends AnyEntity = AnyEntity> =
| FavroResponsePaged<Entity>
| Entity;
export type FavroResponseData<EntityData extends AnyEntityData> =
| FavroResponsePaged<EntityData>
| EntityData;

export type FavroEntityConstructor<EntityData> = new (
client: BravoClient,
data: EntityData,
) => FavroEntity<EntityData>;

0 comments on commit 546cc98

Please sign in to comment.