Separate classes for groups of methods (#1)

* added resourcetype interface that's imported and reused

* added membergroupmembership; comments formatting typedoc output; updated types

* separate classes for groups of methods

* additional methods added, suggested changes made, typedoc documentation improved
This commit is contained in:
Nick Trochalakis
2025-01-11 14:57:09 -08:00
committed by GitHub
parent 27b6abc928
commit 7254e6e5b3
110 changed files with 2199 additions and 1326 deletions
+1
View File
@@ -20,6 +20,7 @@
"plugins": [
"@typescript-eslint"
],
"ignorePatterns": ["d4hWebClient.ts"],
"rules": {
"indent": [
"error",
+12 -1
View File
@@ -1,6 +1,17 @@
import D4H from './src/d4h'
export default D4H
export { Teams, Organisations } from './src/d4h'
export { Qualifications } from './src/api/qualifications'
export { Members } from './src/api/members'
export { Incidents } from './src/api/incidents'
export { Groups } from './src/api/groups'
export { Animals } from './src/api/animals'
export { CustomFields } from './src/api/custom_fields'
export { Roles } from './src/api/roles'
export { Accounts } from './src/api/accounts'
export { Activities } from './src/api/activity'
export * from './src/types/customField'
export * from './src/d4h'
export * from './src/types/group'
+42
View File
@@ -0,0 +1,42 @@
import D4HRequest from '../d4hRequest'
import D4H, { D4H_BASE_URL } from '../d4h'
import { WhoAmIType } from '../types/accounts'
export class Accounts {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @returns - Access details of credentials for a specific context
*/
async whoAmI(context: 'admin' | 'organisation' | 'team', contextId: number): Promise<WhoAmIType> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/whoami`)
try {
const whoami = await this._request.getAsync<WhoAmIType>(url)
return whoami
} catch (error) {
throw new Error('Data not found or improperly formatted.')
}
}
/**
* @returns - Access details of credentials for all available contexts
*/
async whoAmIAll(): Promise<WhoAmIType> {
const url = new URL(`${D4H_BASE_URL}/v3/whoami`)
try {
const whoami = await this._request.getAsync<WhoAmIType>(url)
return whoami
} catch (error) {
throw new Error('Data not found or improperly formatted.')
}
}
}
+143
View File
@@ -0,0 +1,143 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import D4H, { D4H_BASE_URL } from '../d4h'
import { Activity } from '../types/activity';
/** @ignore @inline */
export interface GetActivityAttendOptions {
activity_id?: number | number[];
activity_resource_type?: string;
deleted?: boolean;
ends_after?: string;
ends_before?: string;
id?: number | number[];
member_id?: number | number[];
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
role_id?: number | number[];
size?: number;
sort?: 'createdAt' | 'id' | 'updatedAt'; // default: 'id'
starts_after?: string;
starts_before?: string;
status?: string; // list of ids or null
}
export class Activities {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param attendanceId - The numeric identifier for a resource
* @returns - An animal item
*/
async getActivityAttendance(context: 'admin' | 'organisation' | 'team', contextId: number, attendanceId: number): Promise<Activity> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/attendance/${attendanceId}`)
try {
const activity = await this._request.getAsync<Activity>(url)
activity.entityType = EntityType.Activity
return activity
} catch (error) {
throw new Error('Attendance data not found or improperly formatted.')
}
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.activity_id - One or more activity resource identifiers
* @param options.activity_resource_type - One or more attendance statuses
* @param options.deleted - Return only attendances from deleted activities
* @param options.ends_after - Return only resources ending after this datetime
* @param options.ends_before - Return only resources ending before this datetime
* @param options.id - A list of ids
* @param options.member_id - One or more member resource identifiers
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.role_id - One or more role resource identifiers
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @param options.starts_after - Return only resources starting after this datetime
* @param options.starts_before - Return only resources starting before this datetime
* @param options.status - One or more attendance statuses
*/
async getActivityAttendances(context: 'admin' | 'organisation' | 'team', contextId: number, options?: GetActivityAttendOptions): Promise<Activity[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/attendance`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.activity_id !== undefined) {
optionsList.append('activity_id', options.activity_id.toString())
}
if (options.activity_resource_type !== undefined) {
optionsList.append('activity_resource_type', options.activity_resource_type.toString())
}
if (options.ends_after !== undefined) {
optionsList.append('ends_after', options.ends_after.toString())
}
if (options.ends_before !== undefined) {
optionsList.append('ends_before', options.ends_before.toString())
}
if (options.id !== undefined) {
optionsList.append('id', options.id.toString())
}
if (options.member_id !== undefined) {
optionsList.append('member_id', options.member_id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.role_id !== undefined) {
optionsList.append('role_id', options.role_id.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.starts_before !== undefined) {
optionsList.append('starts_before', options.starts_before.toString())
}
if (options.starts_after !== undefined) {
optionsList.append('starts_after', options.starts_after.toString())
}
if (options.status !== undefined) {
optionsList.append('status', options.status)
}
}
const activity = await this._request.getManyAsync<Activity>(url)
activity.forEach(m => m.entityType = EntityType.Activity)
return activity
}
}
+116
View File
@@ -0,0 +1,116 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import type { Animal } from '../types/animal'
import D4H, { D4H_BASE_URL } from '../d4h'
/** @ignore @inline */
export interface GetAnimalsOptions {
handler_member_id?: number | number[];
id?: number | number[];
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
size?: number;
sort?: 'createdAt' | 'id' | 'updatedAt'; // default: 'id'
status?: 'NON_OPERATIONAL' | 'OPERATIONAL'; // list of ids or null
team_id?: number; // the numeric identifier for a team resource
}
export class Animals {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param animalId - An animal id
* @returns - An animal item
*/
async getAnimal(context: 'admin' | 'organisation' | 'team', contextId: number, animalId: number | 'me'): Promise<Animal> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/animals/${animalId}`)
try {
const animal = await this._request.getAsync<Animal>(url)
animal.entityType = EntityType.Animal
return animal
} catch (error) {
throw new Error('Animal data not found or improperly formatted.')
}
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.handler_member_id - The member id or ids of the animal's handler(s)
* @param options.id - A list of ids
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @param options.status - The status of the animal
* @param options.team_id - The numeric identifier for a resource
* @returns - A list of animals
*/
async getAnimals(context: 'admin' | 'organisation' | 'team', contextId: number, options?: GetAnimalsOptions): Promise<Animal[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/animals`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.handler_member_id !== undefined) {
optionsList.append('handler_member_id', options.handler_member_id.toString())
}
if (options.id !== undefined) {
optionsList.append('id', options.id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.status !== undefined) {
optionsList.append('status', options.status)
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
}
const animals = await this._request.getManyAsync<Animal>(url)
animals.forEach(m => m.entityType = EntityType.Animal)
return animals
}
}
-71
View File
@@ -1,71 +0,0 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import type { Animal } from '../types/animal'
import { GetAnimalsOptions, D4H_BASE_URL } from '../d4h'
export class animalsApi {
static async getAnimal(request: D4HRequest, context: string, contextId: number, animalId: number | 'me'): Promise<Animal> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/animals/${animalId}`)
const animal = await request.getAsync<Animal>(url)
if (!animal) {
throw new Error('Animal data not found or improperly formatted.')
}
animal.entityType = EntityType.Animal
return animal
}
static async getAnimals(request: D4HRequest, context: string, contextId: number, options?: GetAnimalsOptions): Promise<Animal[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/animals`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.handler_member_id !== undefined) {
optionsList.append('handler_member_id', options.handler_member_id.toString())
}
if (options.id !== undefined) {
optionsList.append('id', options.id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => optionsList.append('sort', sortField))
} else {
optionsList.append('sort', options.sort)
}
}
if (options.status !== undefined) {
optionsList.append('status', options.status)
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
}
const animals = await request.getManyAsync<Animal>(url)
animals.forEach(m => m.entityType = EntityType.Animal)
return animals
}
}
+72
View File
@@ -0,0 +1,72 @@
import { CustomFieldUpdate } from '../types/customField'
import { Entity } from '../entity'
import D4H, { D4H_BASE_URL } from '../d4h'
import D4HRequest from '../d4hRequest'
class CustomFields {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
updateCustomFields(entity: Entity, updates: CustomFieldUpdate[], onlyMemberEditOwn: boolean): Promise<void> {
// If no updates, no need to actually make a request. Exit early.
if (updates.length === 0) {
return Promise.resolve()
}
const url = new URL(`${D4H_BASE_URL}/team/custom-fields/${entity.entityType}/${entity.id}`)
// From the documentation:
// https://api.d4h.org/v2/documentation#operation/putTeamCustomfieldsEntity_typeEntity_id
// The PUT action for fields is distinguished by Bundle. If you include one field's value
// from a bundle (or an unbundled field's value) you must include values for all fields
// of that bundle (or all unbundled fields)
// ----------------------------------------------------------------------------------------
// To ensure a mistake doesn't occur, let's ensure that *all* unbundled custom fields from the original
// entity are in the list of updates.
//
// At this time we don't actively use any custom fields in a bundle. To save on the additional complexity,
// bundled custom fields are not supported and will result in an error if attempting to update them.
// At this time all entities we're working with have custom fields. As a result, throw an
// error if none are found. This most likely indicates the original request was made without
// including custom fields.
const originalCustomFields = entity.custom_fields
if (!originalCustomFields) {
throw new Error('Cannot update custom fields for an entity with no custom fields. Ensure your original request included custom fields.')
}
// For any fields not being changed, back fill from the original entity to ensure
// they retain the same value.
for (const field of originalCustomFields) {
const update = updates.find((u) => u.id == field.id)
if (update) {
if (onlyMemberEditOwn && !field.member_edit_own) {
throw new Error('onlyMemberEditOwn specified, but attempting to update non-memberEditOwn field.')
} else if (field.bundle) {
throw new Error('One or more custom fields being updated are part of a bundle. Updating fields in a bundle is not supported.')
}
} else {
let include = true
if (field.bundle || (onlyMemberEditOwn && !field.member_edit_own)) {
include = false
}
if (include) {
updates.push({
id: field.id,
value: field.value
})
}
}
}
return this._request.putAsync(url, { fields: updates })
}
}
export { CustomFields }
+205
View File
@@ -0,0 +1,205 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import D4H, { D4H_BASE_URL } from '../d4h'
import type { groupMembership, memberGroup } from '../types/group'
/** @ignore @inline */
export interface GetMemberGroupsOptions {
id?: number;
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
size?: number;
sort?: 'createdAt' | 'id' | 'updatedAt'; // default: 'id'
team_id?: number | number[];
title?: string;
}
/** @ignore @inline */
export interface GetMemberGroupMembershipOptions {
group_id?: number | number[];
id?: number | number[];
member_id?: number | number[];
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
size?: number;
sort?: 'createdAt' | 'id' | 'updatedAt'; // default: 'id'
team_id?: number | number[];
}
export class Groups {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param groupId - A group identifier
* @returns - A member group
*/
async getMemberGroup(
context: 'admin' | 'organisation' | 'team',
contextId: number,
groupId: number,
): Promise<memberGroup> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-groups/${groupId}`)
try {
const group = await this._request.getAsync<memberGroup>(url)
group.entityType = EntityType.memberGroup
return group
} catch (error) {
throw new Error('Group data not found or improperly formatted.')
}
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.id - A list of ids
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @param options.team_id - One or more team identifiers
* @param options.title - A simple text search term, compared against the title
* @returns
*/
async getMemberGroups(
context: 'admin' | 'organisation' | 'team',
contextId: number,
options?: GetMemberGroupsOptions
): Promise<memberGroup[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-groups`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.id !== undefined) {
optionsList.append('id', options.id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
if (options.title !== undefined) {
optionsList.append('title', options.title)
}
}
const groups = await this._request.getManyAsync<memberGroup>(url)
groups.forEach((m) => (m.entityType = EntityType.memberGroup))
return groups
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.group_id - One or more group identifiers
* @param options.id - A list of ids
* @param options.member_id - One or more member identifiers
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @param options.team_id - One or more team identifiers
* @returns - A list of member group memberships
*/
async getMemberGroupMemberships(
context: 'admin' | 'organisation' | 'team',
contextId: number,
options?: GetMemberGroupMembershipOptions
): Promise<groupMembership[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-group-memberships`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.group_id !== undefined) {
optionsList.append('group_id', options.group_id.toString())
}
if (options.id !== undefined) {
optionsList.append('id_tag', options.id.toString())
}
if (options.member_id !== undefined) {
optionsList.append('member_id', options.member_id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
}
const groups = await this._request.getManyAsync<groupMembership>(url)
groups.forEach((m) => (m.entityType = EntityType.groupMembership))
return groups
}
}
-74
View File
@@ -1,74 +0,0 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import type { memberGroup } from '../types/group'
import { GetMemberGroupsOptions, D4H_BASE_URL } from '../d4h'
export class groupsApi {
static async getMemberGroup(
request: D4HRequest,
context: string,
contextId: number,
groupId: number,
): Promise<memberGroup> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-groups/${groupId}`)
const group = await request.getAsync<memberGroup>(url)
if (!group) {
throw new Error('Group data not found or improperly formatted.')
}
group.entityType = EntityType.memberGroup
return group
}
static async getMemberGroups(
request: D4HRequest,
context: string,
contextId: number,
options?: GetMemberGroupsOptions
): Promise<memberGroup[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-groups`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.id !== undefined) {
optionsList.append('id_tag', options.id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach((sortField) => optionsList.append('sort', sortField))
} else {
optionsList.append('sort', options.sort)
}
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
if (options.title !== undefined) {
optionsList.append('title', options.title)
}
}
const groups = await request.getManyAsync<memberGroup>(url)
groups.forEach((m) => (m.entityType = EntityType.memberGroup))
return groups
}
}
+140
View File
@@ -0,0 +1,140 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import D4H, { D4H_BASE_URL } from '../d4h'
import type { Incident } from '../types/incident'
/** @ignore @inline */
export interface GetIncidentOptions {
after?: string;
before?: string;
deleted?: boolean; // default: 'false'
ends_before?: string;
id?: number | number[];
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
published?: boolean;
reference?: string;
size?: number;
sort?: 'createdAt' | 'id' | 'updatedAt'; // default: 'id'
starts_after?: string;
tag_bundle_id?: number;
tag_id?: number;
team_id?: number;
}
export class Incidents {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param activityId - An activity identifier
* @returns - An incident
*/
async getIncident(context: 'admin' | 'organisation' | 'team', contextId: number, activityId: number): Promise<Incident> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/incidents/${activityId}`)
try {
const incident = await this._request.getAsync<Incident>(url)
incident.entityType = EntityType.Incident
return incident
} catch (error) {
throw new Error('Incident data not found or improperly formatted.')
}
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.after - Return only activities ending after this datetime
* @param options.before - Return only activities starting before this datetime
* @param options.deleted - Return only deleted activities
* @param options.ends_before - Return only activities ending before this datetime
* @param options.id - A list of ids
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.published - Return only published activities
* @param options.reference - A simple text search term, compared against the reference and reference description
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @param options.starts_after - Return only activities starting after this datetime
* @param options.tag_bundle_id - Return only activities with this tag bundle id
* @param options.tag_id - Return only activities with this tag id
* @param options.team_id - Return only activities with this team id
* @returns - A list of incidents
*/
async getIncidents(context: 'admin' | 'organisation' | 'team', contextId: number, options?: GetIncidentOptions): Promise<Incident[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/incidents`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.after !== undefined) {
optionsList.append('after', options.after)
}
if (options.before !== undefined) {
optionsList.append('before', options.before)
}
if (options.deleted !== undefined) {
optionsList.append('deleted', options.deleted.toString())
}
if (options.ends_before !== undefined) {
optionsList.append('ends_before', options.ends_before)
}
if (options.id !== undefined) {
optionsList.append('id', options.id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.published !== undefined) {
optionsList.append('published', options.published.toString())
}
if (options.reference !== undefined) {
optionsList.append('reference', options.reference)
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.starts_after !== undefined) {
optionsList.append('starts_after', options.starts_after)
}
if (options.tag_bundle_id !== undefined) {
optionsList.append('tag_bundle_id', options.tag_bundle_id.toString())
}
if (options.tag_id !== undefined) {
optionsList.append('tag_id', options.tag_id.toString())
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
}
return this._request.getManyAsync(url)
}
}
+178
View File
@@ -0,0 +1,178 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import type { Member } from '../types/member'
import D4H, { D4H_BASE_URL } from '../d4h'
import type { MemberUpdate } from '../types/member'
/** @ignore @inline */
export interface GetMemberOptions {
includeDetails?: boolean;
}
/** @inline */
export interface GetMembersOptions {
deleted?: boolean; // default: false
id?: number | number[]
id_tag?: string;
name?: string;
order?: 'asc' | 'desc'; /** @defaultValue asc */
page?: number;
size?: number;
sort?: 'createdAt' | 'id' | 'updatedAt'; // default: 'id'
statuses?: (number | null)[]; // list of ids or null
team_id?: number; // the numeric identifier for a team resource
}
export class Members {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param id - Member the resource belongs to. Either an id or "me"
* @returns - A member
*/
async getMember(
context: 'admin' | 'organisation' | 'team',
contextId: number,
id: number | 'me',
options?: GetMemberOptions
): Promise<Member> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/members/${id}`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.includeDetails !== undefined) {
optionsList.append('include_details', 'true')
}
}
try {
const member = await this._request.getAsync<Member>(url)
member.entityType = EntityType.Member
return member
} catch (error) {
throw new Error('Member data not found or improperly formatted.')
}
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.deleted - Return only deleted members
* @param options.id - A list of ids
* @param options.id_tag - RFID tag or barcode
* @param options.name - The member's name
* @param options.order -
* @param options.page - Page number
* @param options.size - Items per page
* @param options.sort -
* @param options.statuses - One or more member statuses. Some statuses require extra permissions.
* @param options.team_id - The numeric identifier for a resource
* @returns - A list of members
*/
async getMembers(
context: 'admin' | 'organisation' | 'team',
contextId: number,
options?: GetMembersOptions
): Promise<Member[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/members`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.deleted !== undefined) {
optionsList.append('deleted', options.deleted.toString())
}
if (options.id !== undefined) {
optionsList.append('id', options.id.toString())
}
if (options.id_tag !== undefined) {
optionsList.append('id_tag', options.id_tag)
}
if (options.name !== undefined) {
optionsList.append('name', options.name)
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.statuses !== undefined) {
options.statuses.forEach((status) => {
if (status !== null) {
optionsList.append('statuses', status.toString())
} else {
optionsList.append('statuses', 'null')
}
})
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
}
const members = await this._request.getManyAsync<Member>(url)
members.forEach((m) => (m.entityType = EntityType.Member))
return members
}
/**
*
* @param context
* @param contextId
* @param id
* @param updates
* @returns
*/
updateMember(context: string, contextId: number, id: number, updates: MemberUpdate): Promise<void> {
// If no updates, no need to actually make a request. Exit early.
if (Object.getOwnPropertyNames(updates).length === 0) {
return Promise.resolve()
}
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/members/${id}`)
return this._request.putAsync(url, updates)
}
}
-97
View File
@@ -1,97 +0,0 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import type { Member } from '../types/member'
import { GetMemberOptions, GetMembersOptions, D4H_BASE_URL } from '../d4h'
export class membersApi {
static async getMember(
request: D4HRequest,
context: string,
contextId: number,
id: number | 'me',
options?: GetMemberOptions
): Promise<Member> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/members/${id}`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.includeDetails !== undefined) {
optionsList.append('include_details', 'true')
}
}
const member = await request.getAsync<Member>(url)
if (!member) {
throw new Error('Member data not found or improperly formatted.')
}
member.entityType = EntityType.Member
return member
}
static async getMembers(
request: D4HRequest,
context: string,
contextId: number,
options?: GetMembersOptions
): Promise<Member[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/members`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.deleted !== undefined) {
optionsList.append('deleted', options.deleted.toString())
}
if (options.id_tag !== undefined) {
optionsList.append('id_tag', options.id_tag)
}
if (options.name !== undefined) {
optionsList.append('name', options.name)
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach((sortField) => optionsList.append('sort', sortField))
} else {
optionsList.append('sort', options.sort)
}
}
if (options.statuses !== undefined) {
options.statuses.forEach((status) => {
if (status !== null) {
optionsList.append('statuses', status.toString())
} else {
optionsList.append('statuses', 'null')
}
})
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
}
const members = await request.getManyAsync<Member>(url)
members.forEach((m) => (m.entityType = EntityType.Member))
return members
}
}
+401
View File
@@ -0,0 +1,401 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import type { Qualification, MemberAwards } from '../types/qualification'
import D4H, { D4H_BASE_URL, D4H_FETCH_LIMIT } from '../d4h'
console.log(D4H_FETCH_LIMIT)
/** @ignore @inline */
export interface GetOneQualificationOptions {
exclude_org_data?: boolean;
exclude_teams_data?: boolean;
}
/** @ignore @inline */
export interface GetQualificationOptions {
exclude_org_data?: boolean; // default: false
exclude_teams_data?: boolean; // default: false
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
size?: number;
sort?: 'createdAt' | 'id' | 'updatedAt'; // default: 'id'
title?: string;
}
/** @ignore @inline */
export interface GetMemberAwardsOptions {
exclude_org_data?: boolean; // default: false
exclude_teams_data?: boolean; // default: false
member_id?: number | 'me';
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
qualification_id?: number;
size?: number;
sort?: 'createdAt' | 'id' | 'updatedAt'; // default: 'id'
}
export class Qualifications {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param qualificationId - The qualification's id
* @param options.exclude_org_data - Team context: Exclude entities inherited from the team's org
* @param options.exclude_teams_data - Organisation context: Exclude entities belonging to accessible teams
* @returns - A member qualification
*/
async getMemberQualification(context: 'admin' | 'organisation' | 'team', contextId: number, qualificationId: number | 'me', options?: GetOneQualificationOptions): Promise<Qualification> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-qualifications/${qualificationId}`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
}
try {
const qualification = await this._request.getAsync<Qualification>(url)
qualification.entityType = EntityType.Incident
return qualification
} catch (error) {
throw new Error('Qualification data not found or improperly formatted.')
}
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.exclude_org_data - Team context: Exclude entities inherited from the team's org
* @param options.exclude_teams_data - Organisation context: Exclude entities belonging to accessible teams
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @param options.title - The title of a resource
* @returns - A list of member qualifications
*/
async getMemberQualifications(context: 'admin' | 'organisation' | 'team', contextId: number, options?: GetQualificationOptions): Promise<Qualification[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-qualifications`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.title !== undefined) {
optionsList.append('title', options.title)
}
}
return this._request.getManyAsync(url)
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param qualificationId - The qualification's id
* @param options.exclude_org_data - Team context: Exclude entities inherited from the team's org
* @param options.exclude_teams_data - Organisation context: Exclude entities belonging to accessible teams
* @returns - An animal qualification
*/
async getAnimalQualification(context: 'admin' | 'organisation' | 'team', contextId: number, qualificationId: number, options?: GetOneQualificationOptions): Promise<Qualification> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/animal-qualifications/${qualificationId}`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
}
try {
const qualification = await this._request.getAsync<Qualification>(url)
qualification.entityType = EntityType.Qualification
return qualification
} catch (error) {
throw new Error('Qualification data not found or improperly formatted.')
}
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.exclude_org_data - Team context: Exclude entities inherited from the team's org
* @param options.exclude_teams_data - Organisation context: Exclude entities belonging to accessible teams
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @param options.title - The title of a resource
* @returns - A list of animal qualifications
*/
async getAnimalQualifications(context: 'admin' | 'organisation' | 'team', contextId: number, options?: GetQualificationOptions): Promise<Qualification[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/animal-qualifications`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.title !== undefined) {
optionsList.append('title', options.title)
}
}
const qualifications = await this._request.getManyAsync<Qualification>(url)
qualifications.forEach((m) => (m.entityType = EntityType.Qualification))
return qualifications
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param qualificationId - The qualification's id
* @param options.exclude_org_data - Team context: Exclude entities inherited from the team's org
* @param options.exclude_teams_data - Organisation context: Exclude entities belonging to accessible teams
* @returns - An animal qualification
*/
async getHandlerQualification(context: 'admin' | 'organisation' | 'team', contextId: number, qualificationId: number, options?: GetOneQualificationOptions): Promise<Qualification> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/handler-qualifications/${qualificationId}`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
}
try {
const qualification = await this._request.getAsync<Qualification>(url)
qualification.entityType = EntityType.Qualification
return qualification
} catch (error) {
throw new Error('Qualification data not found or improperly formatted.')
}
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.exclude_org_data - Team context: Exclude entities inherited from the team's org
* @param options.exclude_teams_data - Organisation context: Exclude entities belonging to accessible teams
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @param options.title - The title of a resource
* @returns - A list of animal qualifications
*/
async getHandlerQualifications(context: 'admin' | 'organisation' | 'team', contextId: number, options?: GetQualificationOptions): Promise<Qualification[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/handler-qualifications`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
if (options.title !== undefined) {
optionsList.append('title', options.title)
}
}
const qualifications = await this._request.getManyAsync<Qualification>(url)
qualifications.forEach((m) => (m.entityType = EntityType.Qualification))
return qualifications
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param options.exclude_org_data - Team context: Exclude entities inherited from the team's org
* @param options.exclude_teams_data - Organisation context: Exclude entities belonging to accessible teams
* @param options.member_id - Qualified member. Either an id or "me"
* @param options.order - Default: "asc"
* @param options.page - Page number
* @param options.qualification_id - The qualification's id
* @param options.size - Items per page
* @param options.sort - Default: "id"
* @returns - A list of member qualification awards
*/
async getMemberAwards(context: 'admin' | 'organisation' | 'team', contextId: number, options?: GetMemberAwardsOptions): Promise<MemberAwards[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-qualification-awards`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
if (options.member_id !== undefined) {
optionsList.append('member_id', options.member_id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.qualification_id !== undefined) {
optionsList.append('qualification_id', options.qualification_id.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => {
if (typeof sortField === 'string') {
optionsList.append('sort', sortField)
} else {
console.warn('Skipping invalid sort field: ', sortField)
}
})
} else if (typeof options.sort === 'string') {
optionsList.append('sort', options.sort)
} else {
console.warn('Invalid sort field type:', typeof options.sort)
}
}
}
const awards = await this._request.getManyAsync<MemberAwards>(url)
awards.forEach((m) => (m.entityType = EntityType.Award))
return awards
}
}
-213
View File
@@ -1,213 +0,0 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import type { Qualification, MemberAwards } from '../types/qualification'
import { D4H_BASE_URL, GetQualificationOptions, GetMemberAwardsOptions } from '../d4h'
export class qualificationsApi {
static async getMemberQualification(request: D4HRequest, context: string, contextId: number, qualificationId: number | 'me'): Promise<Qualification> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-qualifications/${qualificationId}`)
const qualification = await request.getAsync<Qualification>(url)
if (!qualification) {
throw new Error('Qualification data not found or improperly formatted.')
}
qualification.entityType = EntityType.Incident
return qualification
}
static async getMemberQualifications(request: D4HRequest, context: string, contextId: number, options?: GetQualificationOptions): Promise<Qualification[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-qualifications`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => optionsList.append('sort', sortField))
} else {
optionsList.append('sort', options.sort)
}
}
if (options.title !== undefined) {
optionsList.append('title', options.title)
}
}
return request.getManyAsync(url)
}
static async getAnimalQualification(request: D4HRequest, context: string, contextId: number, qualificationId: number): Promise<Qualification> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/animal-qualifications/${qualificationId}`)
const qualification = await request.getAsync<Qualification>(url)
if (!qualification) {
throw new Error('Qualification data not found or improperly formatted.')
}
qualification.entityType = EntityType.Qualification
return qualification
}
static async getAnimalQualifications(request: D4HRequest, context: string, contextId: number, options?: GetQualificationOptions): Promise<Qualification[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/animal-qualifications`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => optionsList.append('sort', sortField))
} else {
optionsList.append('sort', options.sort)
}
}
if (options.title !== undefined) {
optionsList.append('title', options.title)
}
}
return request.getManyAsync(url)
}
static async getHandlerQualification(request: D4HRequest, context: string, contextId: number, qualificationId: number): Promise<Qualification> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/handler-qualifications/${qualificationId}`)
const qualification = await request.getAsync<Qualification>(url)
if (!qualification) {
throw new Error('Qualification data not found or improperly formatted.')
}
qualification.entityType = EntityType.Qualification
return qualification
}
static async getHandlerQualifications(request: D4HRequest, context: string, contextId: number, options?: GetQualificationOptions): Promise<Qualification[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/handler-qualifications`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => optionsList.append('sort', sortField))
} else {
optionsList.append('sort', options.sort)
}
}
if (options.title !== undefined) {
optionsList.append('title', options.title)
}
}
return request.getManyAsync(url)
}
static async getMemberAwards(request: D4HRequest, context: string, contextId: number, options?: GetMemberAwardsOptions): Promise<MemberAwards[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/member-qualification-awards`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.exclude_org_data !== undefined) {
optionsList.append('exclude_org_data', options.exclude_org_data.toString())
}
if (options.exclude_teams_data !== undefined) {
optionsList.append('exclude_teams_data', options.exclude_teams_data.toString())
}
if (options.member_id !== undefined) {
optionsList.append('member_id', options.member_id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.qualification_id !== undefined) {
optionsList.append('qualification_id', options.qualification_id.toString())
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => optionsList.append('sort', sortField))
} else {
optionsList.append('sort', options.sort)
}
}
}
const awards = await request.getManyAsync<MemberAwards>(url)
awards.forEach((m) => (m.entityType = EntityType.Award))
return awards
}
}
+34
View File
@@ -0,0 +1,34 @@
import D4HRequest from '../d4hRequest'
import { EntityType } from '../entity'
import D4H, { D4H_BASE_URL, D4H_FETCH_LIMIT } from '../d4h'
import { Role } from '../types/role'
console.log(D4H_FETCH_LIMIT)
export class Roles {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
*
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param roleId - A role id
* @returns - A role
*/
async getMemberQualification(context: 'admin' | 'organisation' | 'team', contextId: number, roleId: number | 'me'): Promise<Role> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/roles/${roleId}`)
try {
const role = await this._request.getAsync<Role>(url)
role.entityType = EntityType.Role
return role
} catch (error) {
throw new Error('Role data not found or improperly formatted.')
}
}
}
+75 -364
View File
@@ -1,383 +1,94 @@
import { CustomFieldUpdate } from './types/customField'
import D4HRequest from './d4hRequest'
import { Entity, EntityType } from './entity'
import type { memberGroup } from './types/group'
import type { Member, MemberUpdate } from './types/member'
import type { Qualification, MemberAwards } from './types/qualification'
import type { Incident } from './types/incident'
import type { Animal } from './types/animal'
import { membersApi } from './api/membersApi'
import { animalsApi } from './api/animalsApi'
import { qualificationsApi } from './api/qualificationsApi'
import { groupsApi } from './api/groupsApi'
import { Team } from './types/team'
import { Organisation } from './types/organisation'
import { EntityType } from './entity'
/** @ignore */
const D4H_FETCH_LIMIT = 250
/** @ignore */
const D4H_BASE_URL = 'https://api.team-manager.us.d4h.com/v3' // Multiple API endpoints now, probably should handle multiple
export { D4H_BASE_URL }
export { D4H_BASE_URL, D4H_FETCH_LIMIT }
export interface GetMemberOptions {
includeDetails?: boolean;
}
export interface GetMembersOptions {
deleted?: boolean; // default: false
id_tag?: string;
name?: string;
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
size?: number;
sort?: string | string[]; // default: 'id'
statuses?: (number | null)[]; // list of ids or null
team_id?: number; // the numeric identifier for a team resource
}
export interface GetAnimalsOptions {
handler_member_id?: number | number[];
id?: number | number[];
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
size?: number;
sort?: string | string[]; // default: 'id'
status?: string; // list of ids or null
team_id?: number; // the numeric identifier for a team resource
}
export interface GetGroupsOptions {
memberId?: number;
title?: string;
}
export interface GetIncidentOptions {
after?: string;
before?: string;
deleted?: boolean; // default: 'false'
ends_before?: string;
id?: number | number[];
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
published?: boolean;
reference?: string;
size?: number;
sort?: string | string[]; // default: 'id'
starts_after?: string;
tag_bundle_id?: number;
tag_id?: number;
team_id?: number;
}
export interface GetQualificationOptions {
exclude_org_data?: boolean; // default: false
exclude_teams_data?: boolean; // default: false
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
size?: number;
sort?: string | string[]; // default: 'id'
title?: string;
}
export interface GetMemberAwardsOptions {
exclude_org_data?: boolean; // default: false
exclude_teams_data?: boolean; // default: false
member_id?: number | 'me';
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
qualification_id?: number;
size?: number;
sort?: string | string[]; // default: 'id'
}
export interface GetMemberGroupsOptions {
id: number;
order?: 'asc' | 'desc'; // default: 'asc'
page?: number;
size?: number;
sort?: string | string[]; // default: 'id'
team_id?: number | number[];
title?: string;
}
export default class D4H {
class D4H {
private readonly _request: D4HRequest
constructor(token: string) {
this._request = new D4HRequest(token, D4H_FETCH_LIMIT)
}
/********************************************/
/**************** MEMBERS *******************/
/********************************************/
async getMember(
context: string,
contextId: number,
id: number | 'me',
options?: GetMemberOptions
): Promise<Member> {
return membersApi.getMember(this._request, context, contextId, id, options)
get request(): D4HRequest {
return this._request
}
async getMembers(
context: string,
contextId: number,
options?: GetMembersOptions
): Promise<Member[]> {
return membersApi.getMembers(this._request, context, contextId, options)
}
class Teams {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
updateMember(context: string, contextId: number, id: number, updates: MemberUpdate): Promise<void> {
// If no updates, no need to actually make a request. Exit early.
if (Object.getOwnPropertyNames(updates).length === 0) {
return Promise.resolve()
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param teamId - A team's id
* @returns - A team
*/
async getTeam(
context: 'admin' | 'organisation' | 'team',
contextId: number,
teamId: number,
): Promise<Team> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/teams/${teamId}`)
try {
const team = await this._request.getAsync<Team>(url)
team.entityType = EntityType.Team
return team
} catch (error) {
throw new Error('Team data not found or improperly formatted.')
}
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/members/${id}`)
return this._request.putAsync(url, updates)
}
/********************************************/
/**************** ANIMALS *******************/
/********************************************/
async getAnimal(
context: string,
contextId: number,
id: number | 'me',
): Promise<Animal> {
return animalsApi.getAnimal(this._request, context, contextId, id)
}
async getAnimals(
context: string,
contextId: number,
options?: GetAnimalsOptions
): Promise<Animal[]> {
return animalsApi.getAnimals(this._request, context, contextId, options)
}
/********************************************/
/*********** EMERGENCY CONTACTS *************/
/********************************************/
// Emergency Contacts no longer have specific endpoint
/********************************************/
/***************** GROUPS *******************/
/********************************************/
async getMemberGroup(
context: string,
contextId: number,
groupId: number,
): Promise<memberGroup> {
return groupsApi.getMemberGroup(this._request, context, contextId, groupId)
}
async getMemberGroups(
context: string,
contextId: number,
options?: GetMemberGroupsOptions
): Promise<memberGroup[]> {
return groupsApi.getMemberGroups(this._request, context, contextId, options)
}
/********************************************/
/**************** INCIDENTS *****************/
/********************************************/
async getIncident(context: string, contextId: number, activityId: number): Promise<Incident> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/incidents/${activityId}`)
const incident = await this._request.getAsync<Incident>(url)
if (!incident) {
throw new Error('Incident data not found or improperly formatted.')
}
incident.entityType = EntityType.Incident
return incident
}
async getIncidents(context: string, contextId: number, options?: GetIncidentOptions): Promise<Incident[]> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/incidents`)
if (options !== undefined) {
const optionsList = url.searchParams
if (options.after !== undefined) {
optionsList.append('after', options.after)
}
if (options.before !== undefined) {
optionsList.append('before', options.before)
}
if (options.deleted !== undefined) {
optionsList.append('order', options.deleted.toString())
}
if (options.ends_before !== undefined) {
optionsList.append('ends_before', options.ends_before)
}
if (options.id !== undefined) {
optionsList.append('id', options.id.toString())
}
if (options.order !== undefined) {
optionsList.append('order', options.order)
}
if (options.page !== undefined) {
optionsList.append('page', options.page.toString())
}
if (options.published !== undefined) {
optionsList.append('published', options.published.toString())
}
if (options.reference !== undefined) {
optionsList.append('reference', options.reference)
}
if (options.size !== undefined) {
optionsList.append('size', options.size.toString())
}
if (options.sort !== undefined) {
if (Array.isArray(options.sort)) {
options.sort.forEach(sortField => optionsList.append('sort', sortField))
} else {
optionsList.append('sort', options.sort)
}
}
if (options.starts_after !== undefined) {
optionsList.append('starts_after', options.starts_after)
}
if (options.tag_bundle_id !== undefined) {
optionsList.append('tag_bundle_id', options.tag_bundle_id.toString())
}
if (options.tag_id !== undefined) {
optionsList.append('tag_id', options.tag_id.toString())
}
if (options.team_id !== undefined) {
optionsList.append('team_id', options.team_id.toString())
}
}
return this._request.getManyAsync(url)
}
/********************************************/
/************* QUALIFICATIONS ***************/
/********************************************/
async getMemberQualification(
context: string,
contextId: number,
id: number | 'me',
): Promise<Qualification> {
return qualificationsApi.getMemberQualification(this._request, context, contextId, id)
}
async getMemberQualifications(
context: string,
contextId: number,
options?: GetQualificationOptions
): Promise<Qualification[]> {
return qualificationsApi.getMemberQualifications(this._request, context, contextId, options)
}
async getAnimalQualification(
context: string,
contextId: number,
id: number,
): Promise<Qualification> {
return qualificationsApi.getAnimalQualification(this._request, context, contextId, id)
}
async getAnimalQualifications(
context: string,
contextId: number,
options?: GetQualificationOptions
): Promise<Qualification[]> {
return qualificationsApi.getAnimalQualifications(this._request, context, contextId, options)
}
async getHandlerQualification(
context: string,
contextId: number,
id: number,
): Promise<Qualification> {
return qualificationsApi.getHandlerQualification(this._request, context, contextId, id)
}
async getHandlerQualifications(
context: string,
contextId: number,
options?: GetQualificationOptions
): Promise<Qualification[]> {
return qualificationsApi.getHandlerQualifications(this._request, context, contextId, options)
}
async getMemberAwards(
context: string,
contextId: number,
options?: GetMemberAwardsOptions
): Promise<MemberAwards[]> {
return qualificationsApi.getMemberAwards(this._request, context, contextId, options)
}
/********************************************/
/************** CUSTOM FIELDS ***************/
/********************************************/
updateCustomFields(entity: Entity, updates: CustomFieldUpdate[], onlyMemberEditOwn: boolean): Promise<void> {
// If no updates, no need to actually make a request. Exit early.
if (updates.length === 0) {
return Promise.resolve()
}
const url = new URL(`${D4H_BASE_URL}/team/custom-fields/${entity.entityType}/${entity.id}`)
// From the documentation:
// https://api.d4h.org/v2/documentation#operation/putTeamCustomfieldsEntity_typeEntity_id
// The PUT action for fields is distinguished by Bundle. If you include one field's value
// from a bundle (or an unbundled field's value) you must include values for all fields
// of that bundle (or all unbundled fields)
// ----------------------------------------------------------------------------------------
// To ensure a mistake doesn't occur, let's ensure that *all* unbundled custom fields from the original
// entity are in the list of updates.
//
// At this time we don't actively use any custom fields in a bundle. To save on the additional complexity,
// bundled custom fields are not supported and will result in an error if attempting to update them.
// At this time all entities we're working with have custom fields. As a result, throw an
// error if none are found. This most likely indicates the original request was made without
// including custom fields.
const originalCustomFields = entity.custom_fields
if (!originalCustomFields) {
throw new Error('Cannot update custom fields for an entity with no custom fields. Ensure your original request included custom fields.')
}
// For any fields not being changed, back fill from the original entity to ensure
// they retain the same value.
for (const field of originalCustomFields) {
const update = updates.find((u) => u.id == field.id)
if (update) {
if (onlyMemberEditOwn && !field.member_edit_own) {
throw new Error('onlyMemberEditOwn specified, but attempting to update non-memberEditOwn field.')
} else if (field.bundle) {
throw new Error('One or more custom fields being updated are part of a bundle. Updating fields in a bundle is not supported.')
}
} else {
let include = true
if (field.bundle || (onlyMemberEditOwn && !field.member_edit_own)) {
include = false
}
if (include) {
updates.push({
id: field.id,
value: field.value
})
}
}
}
return this._request.putAsync(url, { fields: updates })
}
}
class Organisations {
private readonly _request: D4HRequest
constructor(d4hInstance: D4H) {
this._request = d4hInstance.request
}
/**
* @param context - The point of view from where the request takes place
* @param contextId - Either a team, organisation or admin's id
* @param organisationId - An organisation's id
* @returns - A organisation
*/
async getOrganisation(
context: 'admin' | 'organisation' | 'team',
contextId: number,
organisationId: number,
): Promise<Organisation> {
const url = new URL(`${D4H_BASE_URL}/${context}/${contextId}/organisations/${organisationId}`)
try {
const organisation = await this._request.getAsync<Organisation>(url)
organisation.entityType = EntityType.Organisation
return organisation
} catch (error) {
throw new Error('Organisation data not found or improperly formatted.')
}
}
}
export {
Teams,
Organisations,
}
export default D4H
+8 -9
View File
@@ -58,33 +58,32 @@ export default class D4HRequest {
return this.requestAsync<never, DataType>(url, HttpMethod.Get)
}
async getManyAsync<DataType>(url: URL, paginate: boolean = false): Promise<DataType[]> {
async getManyAsync<DataType>(url: URL, paginate = false): Promise<DataType[]> {
let results: DataType[] = []
if (paginate) {
let offset = 0
let hasMore = true
// Pagination loop
while (true) {
while (hasMore) {
const urlWithOffset = new URL(url)
urlWithOffset.searchParams.append('offset', offset.toString())
urlWithOffset.searchParams.append('limit', this._fetchLimit.toString())
const newResults = await this.getAsync<DataType[]>(urlWithOffset)
results = results.concat(newResults)
offset += this._fetchLimit
if (newResults.length < this._fetchLimit) {
break
}
hasMore = newResults.length === this._fetchLimit
}
} else {
// Fetch without pagination
const newResults = await this.getAsync<DataType[]>(url)
results = results.concat(newResults)
}
return results
}
+6 -1
View File
@@ -14,8 +14,13 @@ export interface Entity {
export enum EntityType {
Member = 'member',
memberGroup = 'membergroup',
groupMembership = 'groupmembership',
Incident = 'incident',
Qualification = 'qualification',
Award = 'award',
Animal = 'animal'
Animal = 'animal',
Team = 'team',
Organisation = 'organisation',
Role = 'role',
Activity = 'activity'
}
+31
View File
@@ -0,0 +1,31 @@
export interface Account {
id: number;
resourceType: string;
}
export interface ContextOwner {
resourceType: string;
id: number;
title?: string;
owner?: ContextOwner;
}
export interface Permissions {
[key: string]: {
[action: string]: boolean;
};
}
export interface Context {
id: number;
owner: ContextOwner;
name: string;
hasAccess: boolean;
resourceType: string;
permissions: Permissions;
}
export interface WhoAmIType {
account: Account;
context: Context;
}
+19
View File
@@ -0,0 +1,19 @@
import { Entity } from '../entity'
import { resourceType } from './generic';
export interface Activity extends Entity {
activity: resourceType;
createdAt: string;
duration: number;
endsAt: string;
id: number;
member: resourceType;
owner: resourceType;
startsAt: string;
status: string;
resourceType: string;
role: resourceType;
updatedAt: string;
}
+2
View File
@@ -1,3 +1,4 @@
/** @ignore @inline */
export enum CustomFieldType {
Number = 'number',
Text = 'text',
@@ -14,6 +15,7 @@ export interface CustomField {
member_edit_own: boolean;
}
/** @ignore @inline */
export interface CustomFieldUpdate {
id: number;
value: string | null;
+3
View File
@@ -1,3 +1,5 @@
/** @ignore */
export interface EmergencyContact {
name: string | null
relation: string | null
@@ -5,6 +7,7 @@ export interface EmergencyContact {
alt_phone: string | null
}
/** @ignore */
export interface EmergencyContacts {
primary: EmergencyContact
secondary: EmergencyContact
+9
View File
@@ -0,0 +1,9 @@
//generic.ts
// Reusable interface, used in many other interfaces
/** @ignore @inline */
export interface resourceType {
resourceType: string | null;
id: number | null;
}
+21
View File
@@ -1,4 +1,11 @@
import { Entity } from '../entity'
import { resourceType } from './generic'
/********************************************/
/**************** GROUPS ********************/
/********************************************/
export interface memberGroup extends Entity{
id: number;
@@ -8,4 +15,18 @@ export interface memberGroup extends Entity{
resourceType: string;
createdAt: string;
updatedAt: string;
}
/********************************************/
/*********** GROUP MEMBERSHIP ***************/
/********************************************/
export interface groupMembership extends Entity{
id: number;
owner: resourceType;
group: resourceType;
member: resourceType;
}
+7 -19
View File
@@ -1,21 +1,20 @@
import { Entity } from '../entity'
import { resourceType } from './generic'
export interface IncidentOwner {
resourceType: string;
id: number;
}
/** @ignore @inline */
export interface IncidentLocation {
type: string;
coordinates: [number, number];
}
/** @ignore @inline */
export interface WeatherInfo {
symbol: string | null;
symbolDate: string | null;
temperature: number | null;
}
/** @ignore @inline */
export interface AddressInfo {
postCode: string;
region: string;
@@ -23,20 +22,9 @@ export interface AddressInfo {
town: string;
country: string;
}
export interface IncidentLocationBookmark {
id: number | null;
resourceType: string;
}
export interface IncidentTag {
id: number;
resourceType: string;
}
export interface Incident extends Entity{
id: number;
owner: IncidentOwner;
owner: resourceType;
resourceType: string;
reference: string;
referenceDescription: string;
@@ -65,8 +53,8 @@ export interface Incident extends Entity{
weatherCloudCover: number | null;
address: AddressInfo;
location: IncidentLocation;
locationBookmark: IncidentLocationBookmark;
locationBookmark: resourceType;
fullTeam: boolean;
selfCoordinator: boolean;
tags: IncidentTag[];
tags: resourceType[];
}
+16 -40
View File
@@ -1,5 +1,7 @@
import { Entity } from '../entity'
import { resourceType } from './generic'
/** @ignore @inline */
export interface MemberStatus {
id: number
type: string
@@ -7,65 +9,39 @@ export interface MemberStatus {
label: MemberStatusLabel | null
}
/** @ignore @inline */
export interface MemberStatusLabel {
id: number
value: string
}
export interface CustomMemberStatus {
id: number | null
resourceType: string
}
export interface EquipmentLocation {
id: number | null
resourceType: string
}
/** @ignore @inline */
export interface EmailInfo {
value: string
verified: boolean
}
/** @ignore @inline */
export interface PhoneInfo {
phone: string
verified?: boolean
}
export interface PrimaryEmergencyContact {
/** @ignore @inline */
export interface emergencyContact {
name: string
primaryPhone: string
secondaryPhone: string
relation: string
}
export interface SecondaryEmergencyContact {
name: string
primaryPhone: string
secondaryPhone: string
relation: string
}
export interface RetiredReason {
id: number | null
resourceType: string
}
export interface Role {
id: number | null
resourceType: string
}
export interface MemberLocationBookmark {
id: number | null
resourceType: string
}
/** @inline */
export interface Location {
type: string
coordinates: [number, number]
}
export interface Member extends Entity {
id: number
name: string
@@ -90,14 +66,14 @@ export interface Member extends Entity {
permission: number
credits: number
defaultDuty: string
defaultEquipmentLocation: EquipmentLocation
customStatus: CustomMemberStatus
defaultEquipmentLocation: resourceType
customStatus: resourceType
location: Location
locationBookmark: MemberLocationBookmark
retiredReason: RetiredReason
role: Role
primaryEmergencyContact: PrimaryEmergencyContact
secondaryEmergencyContact: SecondaryEmergencyContact
locationBookmark: resourceType
retiredReason: resourceType
role: resourceType
primaryEmergencyContact: emergencyContact
secondaryEmergencyContact: emergencyContact
alertActivityApproval: boolean
alertAllQualifications: boolean
alertGear: boolean
+12
View File
@@ -0,0 +1,12 @@
import { Entity } from '../entity'
/** @ignore @inline */
export interface Organisation extends Entity {
id: number,
title: string,
country: string,
timezone: string,
createdAt: string,
updatedAt: string,
resourceType: string,
}
+4 -8
View File
@@ -1,9 +1,5 @@
import { Entity } from '../entity'
export interface Resource {
resourceType: string;
id: number;
}
import { resourceType } from './generic'
export interface Qualification extends Entity {
id: number,
@@ -26,7 +22,7 @@ export interface MemberAwards extends Entity {
resourceType: string;
startsAt: string | null;
endsAt: string | null;
owner: Resource;
member: Resource;
qualification: Resource;
owner: resourceType;
member: resourceType;
qualification: resourceType;
}
+20
View File
@@ -0,0 +1,20 @@
import { Entity } from '../entity'
import { resourceType } from './generic'
export interface costs {
hour: number,
use: number
}
export interface Role extends Entity{
id: number,
title: string,
deprecatedBundle: string,
owner: resourceType,
order: number,
cost: costs,
resourceType: string,
createdAt: string,
upatedAt: string
}
+34
View File
@@ -0,0 +1,34 @@
import { Entity } from '../entity'
import { resourceType } from './generic'
/** @ignore @inline */
export interface location {
type: string,
coordinates: [number, number],
}
/** @ignore @inline */
export interface counts {
total: number,
operational: number,
}
/** @ignore @inline */
export interface Team extends Entity {
id: number,
owner: resourceType,
title: string,
location: location,
memberCounts: counts,
timezone: string,
country: string,
deletedAt: null,
overview: string,
createdAt: string,
updatedAt: string,
subdomain: string,
resourceType: string,
}