From e552ff377ee22b2a4c35b0dd2888cc774fe89899 Mon Sep 17 00:00:00 2001 From: Colin Williams <126836598+colincwilliams-kcsara@users.noreply.github.com> Date: Sun, 19 Mar 2023 18:01:57 -0700 Subject: [PATCH] Added support for updating custom fields. --- lib/src/customField.ts | 20 ++++++++++++ lib/src/d4h.ts | 71 +++++++++++++++++++++++++++++++++++++--- lib/src/entity.ts | 16 +++++++++ lib/src/httpUtils.ts | 2 +- lib/src/member.ts | 18 ++--------- test/index.ts | 73 ++++++++++++++++++++++++++++++------------ test/package.json | 3 +- 7 files changed, 159 insertions(+), 44 deletions(-) create mode 100644 lib/src/customField.ts create mode 100644 lib/src/entity.ts diff --git a/lib/src/customField.ts b/lib/src/customField.ts new file mode 100644 index 0000000..2d8d07b --- /dev/null +++ b/lib/src/customField.ts @@ -0,0 +1,20 @@ +export enum CustomFieldType { + Number = 'number', + Text = 'text', + Date = 'date', +} + +export interface CustomField { + id: number; + type: CustomFieldType; + label: string; + value_string: string | null; + value: string | null; + bundle: unknown | null; + member_edit_own: boolean; +} + +export interface CustomFieldUpdate { + id: number; + value: string | null; +} \ No newline at end of file diff --git a/lib/src/d4h.ts b/lib/src/d4h.ts index a2546c4..2f09fbf 100644 --- a/lib/src/d4h.ts +++ b/lib/src/d4h.ts @@ -1,5 +1,7 @@ -import HttpUtils from './httpUtils' +import { CustomFieldUpdate } from './customField' +import { Entity, EntityType } from './entity' import type { Group } from './group' +import HttpUtils from './httpUtils' import type { Member, MemberUpdate } from './member' const D4H_FETCH_LIMIT = 250 @@ -31,7 +33,7 @@ export default class D4H { /**************** MEMBERS *******************/ /********************************************/ - getMemberAsync(id: number, options?: GetMemberOptions): Promise { + async getMemberAsync(id: number, options?: GetMemberOptions): Promise { const url = new URL(`${D4H_BASE_URL}/team/members/${id}`) if (options !== undefined) { @@ -42,10 +44,13 @@ export default class D4H { } } - return this._httpUtils.getAsync(url) + const member = await this._httpUtils.getAsync(url) + member.type = EntityType.Member + + return member } - getMembersAsync(options?: GetMembersOptions): Promise { + async getMembersAsync(options?: GetMembersOptions): Promise { const url = new URL(`${D4H_BASE_URL}/team/members`) if (options !== undefined) { @@ -64,7 +69,10 @@ export default class D4H { } } - return this._httpUtils.getManyAsync(url) + const members = await this._httpUtils.getManyAsync(url) + members.forEach(m => m.type = EntityType.Member) + + return members } updateMemberAsync(id: number, updates: MemberUpdate): Promise { @@ -98,4 +106,57 @@ export default class D4H { return this._httpUtils.getManyAsync(url) } + + /********************************************/ + /************** CUSTOM FIELDS ***************/ + /********************************************/ + + updateCustomFields(entity: Entity, updates: CustomFieldUpdate[], onlyMemberEditOwn: boolean): Promise { + const url = new URL(`${D4H_BASE_URL}/team/custom-fields/${entity.type}/${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* custom fields from the original + // entity are in the list of updates. + + // 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) { + if (field.bundle !== null) { + throw new Error('One or more custom fields on the entity are part of a bundle. Bundles are not supported.') + } + + const update = updates.find((u) => u.id == field.id) + if (update && onlyMemberEditOwn && !field.member_edit_own) { + throw new Error('onlyMemberEditOwn specified, but attempting to update non-memberEditOwn field.') + } else if (!update) { + let include = true + if (onlyMemberEditOwn && !field.member_edit_own) { + include = false + } + + if (include) { + updates.push({ + id: field.id, + value: field.value + }) + } + } + } + + return this._httpUtils.putAsync(url, { fields: updates }) + } } \ No newline at end of file diff --git a/lib/src/entity.ts b/lib/src/entity.ts new file mode 100644 index 0000000..d9bf41f --- /dev/null +++ b/lib/src/entity.ts @@ -0,0 +1,16 @@ +import { CustomField } from './customField' + +export interface Entity { + custom_fields?: CustomField[]; + id: number; + type: EntityType; +} + +// EntityType must be one of: +// event, exercise, gear, healthsafety_report, incident, +// incident.weather, member, person_involved, unit, activity +// +// Only the ones actively in use are implemented. +export enum EntityType { + Member = 'member', +} diff --git a/lib/src/httpUtils.ts b/lib/src/httpUtils.ts index ff6ed6a..4c2a6c2 100644 --- a/lib/src/httpUtils.ts +++ b/lib/src/httpUtils.ts @@ -33,7 +33,7 @@ export default class HttpUtils { 'Content-Type': 'application/json' } - console.log(url) + console.log(`${method}: ${url.toString()}\n${JSON.stringify(body)}`) const options: RequestInit = { method, diff --git a/lib/src/member.ts b/lib/src/member.ts index b08652a..0ca4b35 100644 --- a/lib/src/member.ts +++ b/lib/src/member.ts @@ -1,16 +1,4 @@ -export enum CustomFieldType { - Number = 'number', - Text = 'text', - Date = 'date', -} - -export interface CustomField { - id: number; - type: CustomFieldType; - label: string; - value_string: string | null; - value: string | null; -} +import { Entity } from './entity' export interface EmergencyContact { name: string | null; @@ -31,14 +19,12 @@ export interface MemberStatusLabel { value: string; } -export interface Member { +export interface Member extends Entity { address: string; - custom_fields?: CustomField[]; email: string | null; emergency_contacts: EmergencyContact[]; group_ids: number[] | null; homephone: string; - id: number; mobilephone: string; name: string; notes: string | null; diff --git a/test/index.ts b/test/index.ts index c7a0df1..e624b78 100644 --- a/test/index.ts +++ b/test/index.ts @@ -15,32 +15,63 @@ let getMembersOptions: GetMembersOptions = { }; let main = async function() { + const testGetMembers = false + const testGetGroups = false + const testGetMember = false + // Only execute update code on an instance where that's OK. // Double-check your API key. Double-check what you're modifying. - /*try { - const memberId = 0000 // Set this to a real ID - await api.updateMemberAsync(memberId, { - notes: null, - }) - } catch (err) { - console.log(JSON.stringify(err)); - return; - }*/ + // try { + // const memberId = 0000 // Set this to a real ID + // await api.updateMemberAsync(memberId, { + // notes: null, + // }) + // } catch (err) { + // console.log(JSON.stringify(err)) + // return + // } - try { - const members = await api.getMembersAsync(getMembersOptions); - console.log(`Retrieved ${members.length} members.`); - } catch (err) { - console.log(JSON.stringify(err)); - return; + if (testGetMembers) { + try { + const members = await api.getMembersAsync(getMembersOptions) + console.log(`Retrieved ${members.length} members.`) + } catch (err) { + console.log(JSON.stringify(err)) + return + } } - try { - const groups = await api.getGroupsAsync(); - console.log(`Retrieved ${groups.length} groups.`); - } catch (err) { - console.log(JSON.stringify(err)); - return; + if (testGetGroups) { + try { + const groups = await api.getGroupsAsync() + console.log(`Retrieved ${groups.length} groups.`) + } catch (err) { + console.log(JSON.stringify(err)) + return + } + } + + if (testGetMember) { + try { + const memberId = 103636 + const member = await api.getMemberAsync(memberId, { includeDetails: true }) + console.log(`Retrieved single member: ${member.name}`) + + // Only execute update code on an instance where that's OK. + // Double-check your API key. Double-check what you're modifying. + // const unitStatusField = member.custom_fields?.find(f => f.label === "Secondary Email") + // if (unitStatusField) { + // await api.updateCustomFields( + // member, + // [{ id: unitStatusField.id, value: "test email" }], + // /* onlyMemberEditOwn */ true) + // } else { + // throw new Error('Failed to find custom field') + // } + } catch(err) { + console.log((err as Error).message); + return + } } } diff --git a/test/package.json b/test/package.json index 234b365..601f93a 100644 --- a/test/package.json +++ b/test/package.json @@ -4,7 +4,8 @@ "main": "./built/index.js", "license": "MIT", "scripts": { - "build": "npx tsc", + "build": "npm run build-lib && npx tsc", + "build-lib": "cd ../lib && npm run build", "launch": "npm run build && node ./built" }, "devDependencies": {