@rachelallyson/planning-center-people-ts 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,119 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.5.0] - 2025-01-10
9
+
10
+ ### 🎯 **NEW FEATURES - Person Relationship Management & Token Refresh Fix**
11
+
12
+ This release introduces comprehensive person relationship management endpoints and fixes critical token refresh issues, significantly enhancing the library's functionality and reliability.
13
+
14
+ ### Added
15
+
16
+ #### **👥 Person Relationship Management**
17
+
18
+ - **🏢 Campus Management**: Complete campus assignment and retrieval system
19
+ - `getPrimaryCampus(personId)` - Get person's current campus
20
+ - `setPrimaryCampus(personId, campusId)` - Assign/update person's campus
21
+ - `removePrimaryCampus(personId)` - Remove campus assignment
22
+ - `getByCampus(campusId, options)` - Get all people in a campus
23
+
24
+ - **🏠 Household Management**: Full household membership system
25
+ - `getHousehold(personId)` - Get person's household
26
+ - `setHousehold(personId, householdId)` - Assign person to household
27
+ - `removeFromHousehold(personId)` - Remove person from household
28
+ - `getHouseholdMembers(householdId, options)` - Get all household members
29
+
30
+ - **📋 Related Data Access**: Comprehensive access to person-related data
31
+ - `getWorkflowCards(personId, options)` - Get person's workflow cards
32
+ - `getNotes(personId, options)` - Get person's notes
33
+ - `getFieldData(personId, options)` - Get person's field data
34
+ - `getSocialProfiles(personId, options)` - Get person's social profiles
35
+
36
+ #### **🔧 Enhanced Type System**
37
+
38
+ - **📝 Complete PersonRelationships**: Updated interface with all available relationships
39
+ - **🏷️ Type Safety**: Full TypeScript support for all relationship operations
40
+ - **🛡️ Null Handling**: Proper handling of optional relationships
41
+ - **📊 Resource Validation**: Enhanced relationship data validation
42
+
43
+ #### **🔐 Token Refresh Fix**
44
+
45
+ - **🚫 Fixed 401 Unauthorized**: Resolved token refresh failures by including client credentials
46
+ - **🔑 Client Credentials Support**: Added support for `clientId` and `clientSecret` in OAuth config
47
+ - **🌍 Environment Variables**: Support for `PCO_APP_ID` and `PCO_APP_SECRET` environment variables
48
+ - **🔄 Standardized Implementation**: Consistent token refresh across all HTTP clients
49
+ - **🛡️ Enhanced Error Handling**: Better error messages for token refresh failures
50
+
51
+ ### Fixed
52
+
53
+ - **🔐 Token Refresh 401 Errors**: Fixed "Token refresh failed: 401 Unauthorized" by including required client credentials
54
+ - **🏗️ Missing Auth Types**: Added missing `BasicAuth` type to v2.0.0 client configuration
55
+ - **🔄 Inconsistent Implementations**: Standardized token refresh across `auth.ts` and `http.ts`
56
+ - **📝 Type Definitions**: Enhanced `PersonRelationships` interface with all available relationships
57
+
58
+ ### Usage Examples
59
+
60
+ ```typescript
61
+ // Campus Management
62
+ const campus = await client.people.getPrimaryCampus('person-123');
63
+ await client.people.setPrimaryCampus('person-123', 'campus-456');
64
+
65
+ // Household Management
66
+ const household = await client.people.getHousehold('person-123');
67
+ await client.people.setHousehold('person-123', 'household-789');
68
+
69
+ // Related Data Access
70
+ const workflowCards = await client.people.getWorkflowCards('person-123');
71
+ const notes = await client.people.getNotes('person-123');
72
+ const fieldData = await client.people.getFieldData('person-123');
73
+
74
+ // Token Refresh with Client Credentials
75
+ const client = new PcoClient({
76
+ auth: {
77
+ type: 'oauth',
78
+ accessToken: 'your-token',
79
+ refreshToken: 'your-refresh-token',
80
+ clientId: 'your-app-id', // NEW: Client credentials
81
+ clientSecret: 'your-app-secret', // NEW: Client credentials
82
+ onRefresh: async (tokens) => { /* handle refresh */ },
83
+ onRefreshFailure: async (error) => { /* handle failure */ }
84
+ }
85
+ });
86
+ ```
87
+
88
+ ### Migration Guide
89
+
90
+ **From Direct API Calls:**
91
+
92
+ ```typescript
93
+ // Before: Complex direct API calls
94
+ const response = await client.httpClient.request({
95
+ method: 'PATCH',
96
+ endpoint: `/people/${personId}`,
97
+ data: { /* complex JSON structure */ }
98
+ });
99
+
100
+ // After: Simple, intuitive methods
101
+ await client.people.setPrimaryCampus(personId, campusId);
102
+ ```
103
+
104
+ **Token Refresh Configuration:**
105
+
106
+ ```typescript
107
+ // Add client credentials to your OAuth configuration
108
+ const client = new PcoClient({
109
+ auth: {
110
+ type: 'oauth',
111
+ accessToken: 'your-token',
112
+ refreshToken: 'your-refresh-token',
113
+ clientId: process.env.PCO_APP_ID, // NEW
114
+ clientSecret: process.env.PCO_APP_SECRET, // NEW
115
+ onRefresh: async (tokens) => { /* save tokens */ },
116
+ onRefreshFailure: async (error) => { /* handle failure */ }
117
+ }
118
+ });
119
+ ```
120
+
8
121
  ## [2.4.0] - 2025-01-10
9
122
 
10
123
  ### 🎯 **NEW FEATURES - Age Preference Matching & Exact Name Matching**
package/dist/core/http.js CHANGED
@@ -191,6 +191,11 @@ class PcoHttpClient {
191
191
  else if (this.config.auth.type === 'oauth') {
192
192
  headers.Authorization = `Bearer ${this.config.auth.accessToken}`;
193
193
  }
194
+ else if (this.config.auth.type === 'basic') {
195
+ // Basic auth with app credentials
196
+ const credentials = Buffer.from(`${this.config.auth.appId}:${this.config.auth.appSecret}`).toString('base64');
197
+ headers.Authorization = `Basic ${credentials}`;
198
+ }
194
199
  }
195
200
  getResourceTypeFromEndpoint(endpoint) {
196
201
  // Extract resource type from endpoint
@@ -224,18 +229,29 @@ class PcoHttpClient {
224
229
  }
225
230
  const baseURL = this.config.baseURL || 'https://api.planningcenteronline.com/people/v2';
226
231
  const tokenUrl = baseURL.replace('/people/v2', '/oauth/token');
232
+ // Prepare the request body for token refresh
233
+ const body = new URLSearchParams({
234
+ grant_type: 'refresh_token',
235
+ refresh_token: this.config.auth.refreshToken,
236
+ });
237
+ // Add client credentials if available from the config or environment
238
+ const clientId = this.config.auth.clientId || process.env.PCO_APP_ID;
239
+ const clientSecret = this.config.auth.clientSecret || process.env.PCO_APP_SECRET;
240
+ if (clientId && clientSecret) {
241
+ body.append('client_id', clientId);
242
+ body.append('client_secret', clientSecret);
243
+ }
227
244
  const response = await fetch(tokenUrl, {
228
245
  method: 'POST',
229
246
  headers: {
230
247
  'Content-Type': 'application/x-www-form-urlencoded',
248
+ 'Accept': 'application/json',
231
249
  },
232
- body: new URLSearchParams({
233
- grant_type: 'refresh_token',
234
- refresh_token: this.config.auth.refreshToken,
235
- }),
250
+ body: body.toString(),
236
251
  });
237
252
  if (!response.ok) {
238
- throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
253
+ const errorData = await response.json().catch(() => ({}));
254
+ throw new Error(`Token refresh failed: ${response.status} ${response.statusText}. ${JSON.stringify(errorData)}`);
239
255
  }
240
256
  const tokens = await response.json();
241
257
  // Update the config with new tokens
@@ -6,7 +6,7 @@ import type { PcoHttpClient } from '../core/http';
6
6
  import type { PaginationHelper } from '../core/pagination';
7
7
  import type { PcoEventEmitter } from '../monitoring';
8
8
  import type { PaginationOptions, PaginationResult } from '../core/pagination';
9
- import type { PersonResource, EmailResource, EmailAttributes, PhoneNumberResource, PhoneNumberAttributes, AddressResource, AddressAttributes, SocialProfileResource, SocialProfileAttributes } from '../types';
9
+ import type { PersonResource, EmailResource, EmailAttributes, PhoneNumberResource, PhoneNumberAttributes, AddressResource, AddressAttributes, SocialProfileResource, SocialProfileAttributes, CampusResource, HouseholdResource } from '../types';
10
10
  export interface PeopleListOptions {
11
11
  where?: Record<string, any>;
12
12
  include?: string[];
@@ -86,6 +86,94 @@ export declare class PeopleModule extends BaseModule {
86
86
  * Delete a person
87
87
  */
88
88
  delete(id: string): Promise<void>;
89
+ /**
90
+ * Get a person's primary campus
91
+ */
92
+ getPrimaryCampus(personId: string): Promise<CampusResource | null>;
93
+ /**
94
+ * Set a person's primary campus
95
+ */
96
+ setPrimaryCampus(personId: string, campusId: string): Promise<PersonResource>;
97
+ /**
98
+ * Remove a person's primary campus
99
+ */
100
+ removePrimaryCampus(personId: string): Promise<PersonResource>;
101
+ /**
102
+ * Get a person's household
103
+ */
104
+ getHousehold(personId: string): Promise<HouseholdResource | null>;
105
+ /**
106
+ * Set a person's household
107
+ */
108
+ setHousehold(personId: string, householdId: string): Promise<PersonResource>;
109
+ /**
110
+ * Remove a person from their household
111
+ */
112
+ removeFromHousehold(personId: string): Promise<PersonResource>;
113
+ /**
114
+ * Get all people in a specific household
115
+ */
116
+ getHouseholdMembers(householdId: string, options?: PeopleListOptions): Promise<{
117
+ data: PersonResource[];
118
+ meta?: any;
119
+ links?: any;
120
+ }>;
121
+ /**
122
+ * Get people by campus
123
+ */
124
+ getByCampus(campusId: string, options?: PeopleListOptions): Promise<{
125
+ data: PersonResource[];
126
+ meta?: any;
127
+ links?: any;
128
+ }>;
129
+ /**
130
+ * Get a person's workflow cards
131
+ */
132
+ getWorkflowCards(personId: string, options?: {
133
+ include?: string[];
134
+ perPage?: number;
135
+ page?: number;
136
+ }): Promise<{
137
+ data: any[];
138
+ meta?: any;
139
+ links?: any;
140
+ }>;
141
+ /**
142
+ * Get a person's notes
143
+ */
144
+ getNotes(personId: string, options?: {
145
+ include?: string[];
146
+ perPage?: number;
147
+ page?: number;
148
+ }): Promise<{
149
+ data: any[];
150
+ meta?: any;
151
+ links?: any;
152
+ }>;
153
+ /**
154
+ * Get a person's field data
155
+ */
156
+ getFieldData(personId: string, options?: {
157
+ include?: string[];
158
+ perPage?: number;
159
+ page?: number;
160
+ }): Promise<{
161
+ data: any[];
162
+ meta?: any;
163
+ links?: any;
164
+ }>;
165
+ /**
166
+ * Get a person's social profiles
167
+ */
168
+ getSocialProfiles(personId: string, options?: {
169
+ include?: string[];
170
+ perPage?: number;
171
+ page?: number;
172
+ }): Promise<{
173
+ data: any[];
174
+ meta?: any;
175
+ links?: any;
176
+ }>;
89
177
  /**
90
178
  * Find or create a person with smart matching
91
179
  */
@@ -164,14 +252,6 @@ export declare class PeopleModule extends BaseModule {
164
252
  * Delete a person's address
165
253
  */
166
254
  deleteAddress(personId: string, addressId: string): Promise<void>;
167
- /**
168
- * Get person's social profiles
169
- */
170
- getSocialProfiles(personId: string): Promise<{
171
- data: SocialProfileResource[];
172
- meta?: any;
173
- links?: any;
174
- }>;
175
255
  /**
176
256
  * Add a social profile to a person
177
257
  */
@@ -75,6 +75,209 @@ class PeopleModule extends base_1.BaseModule {
75
75
  async delete(id) {
76
76
  return this.deleteResource(`/people/${id}`);
77
77
  }
78
+ // ===== Relationship Management =====
79
+ /**
80
+ * Get a person's primary campus
81
+ */
82
+ async getPrimaryCampus(personId) {
83
+ const person = await this.getById(personId, ['primary_campus']);
84
+ const campusData = person.relationships?.primary_campus?.data;
85
+ if (!campusData || Array.isArray(campusData) || !campusData.id) {
86
+ return null;
87
+ }
88
+ // Get the full campus resource
89
+ return this.httpClient.request({
90
+ method: 'GET',
91
+ endpoint: `/campuses/${campusData.id}`
92
+ }).then(response => response.data);
93
+ }
94
+ /**
95
+ * Set a person's primary campus
96
+ */
97
+ async setPrimaryCampus(personId, campusId) {
98
+ return this.httpClient.request({
99
+ method: 'PATCH',
100
+ endpoint: `/people/${personId}`,
101
+ data: {
102
+ data: {
103
+ type: 'Person',
104
+ id: personId,
105
+ attributes: {
106
+ primary_campus_id: campusId
107
+ }
108
+ }
109
+ }
110
+ }).then(response => response.data);
111
+ }
112
+ /**
113
+ * Remove a person's primary campus
114
+ */
115
+ async removePrimaryCampus(personId) {
116
+ return this.httpClient.request({
117
+ method: 'PATCH',
118
+ endpoint: `/people/${personId}`,
119
+ data: {
120
+ data: {
121
+ type: 'Person',
122
+ id: personId,
123
+ attributes: {
124
+ primary_campus_id: null
125
+ }
126
+ }
127
+ }
128
+ }).then(response => response.data);
129
+ }
130
+ /**
131
+ * Get a person's household
132
+ */
133
+ async getHousehold(personId) {
134
+ const person = await this.getById(personId, ['household']);
135
+ const householdData = person.relationships?.household?.data;
136
+ if (!householdData || Array.isArray(householdData) || !householdData.id) {
137
+ return null;
138
+ }
139
+ // Get the full household resource
140
+ return this.httpClient.request({
141
+ method: 'GET',
142
+ endpoint: `/households/${householdData.id}`
143
+ }).then(response => response.data);
144
+ }
145
+ /**
146
+ * Set a person's household
147
+ */
148
+ async setHousehold(personId, householdId) {
149
+ return this.httpClient.request({
150
+ method: 'PATCH',
151
+ endpoint: `/people/${personId}`,
152
+ data: {
153
+ data: {
154
+ type: 'Person',
155
+ id: personId,
156
+ attributes: {
157
+ household_id: householdId
158
+ }
159
+ }
160
+ }
161
+ }).then(response => response.data);
162
+ }
163
+ /**
164
+ * Remove a person from their household
165
+ */
166
+ async removeFromHousehold(personId) {
167
+ return this.httpClient.request({
168
+ method: 'PATCH',
169
+ endpoint: `/people/${personId}`,
170
+ data: {
171
+ data: {
172
+ type: 'Person',
173
+ id: personId,
174
+ attributes: {
175
+ household_id: null
176
+ }
177
+ }
178
+ }
179
+ }).then(response => response.data);
180
+ }
181
+ /**
182
+ * Get all people in a specific household
183
+ */
184
+ async getHouseholdMembers(householdId, options = {}) {
185
+ const params = {
186
+ 'where[household_id]': householdId
187
+ };
188
+ if (options.include) {
189
+ params.include = options.include.join(',');
190
+ }
191
+ if (options.perPage) {
192
+ params.per_page = options.perPage;
193
+ }
194
+ if (options.page) {
195
+ params.page = options.page;
196
+ }
197
+ return this.getList('/people', params);
198
+ }
199
+ /**
200
+ * Get people by campus
201
+ */
202
+ async getByCampus(campusId, options = {}) {
203
+ const params = {
204
+ 'where[primary_campus_id]': campusId
205
+ };
206
+ if (options.include) {
207
+ params.include = options.include.join(',');
208
+ }
209
+ if (options.perPage) {
210
+ params.per_page = options.perPage;
211
+ }
212
+ if (options.page) {
213
+ params.page = options.page;
214
+ }
215
+ return this.getList('/people', params);
216
+ }
217
+ /**
218
+ * Get a person's workflow cards
219
+ */
220
+ async getWorkflowCards(personId, options = {}) {
221
+ const params = {};
222
+ if (options.include) {
223
+ params.include = options.include.join(',');
224
+ }
225
+ if (options.perPage) {
226
+ params.per_page = options.perPage;
227
+ }
228
+ if (options.page) {
229
+ params.page = options.page;
230
+ }
231
+ return this.getList(`/people/${personId}/workflow_cards`, params);
232
+ }
233
+ /**
234
+ * Get a person's notes
235
+ */
236
+ async getNotes(personId, options = {}) {
237
+ const params = {};
238
+ if (options.include) {
239
+ params.include = options.include.join(',');
240
+ }
241
+ if (options.perPage) {
242
+ params.per_page = options.perPage;
243
+ }
244
+ if (options.page) {
245
+ params.page = options.page;
246
+ }
247
+ return this.getList(`/people/${personId}/notes`, params);
248
+ }
249
+ /**
250
+ * Get a person's field data
251
+ */
252
+ async getFieldData(personId, options = {}) {
253
+ const params = {};
254
+ if (options.include) {
255
+ params.include = options.include.join(',');
256
+ }
257
+ if (options.perPage) {
258
+ params.per_page = options.perPage;
259
+ }
260
+ if (options.page) {
261
+ params.page = options.page;
262
+ }
263
+ return this.getList(`/people/${personId}/field_data`, params);
264
+ }
265
+ /**
266
+ * Get a person's social profiles
267
+ */
268
+ async getSocialProfiles(personId, options = {}) {
269
+ const params = {};
270
+ if (options.include) {
271
+ params.include = options.include.join(',');
272
+ }
273
+ if (options.perPage) {
274
+ params.per_page = options.perPage;
275
+ }
276
+ if (options.page) {
277
+ params.page = options.page;
278
+ }
279
+ return this.getList(`/people/${personId}/social_profiles`, params);
280
+ }
78
281
  /**
79
282
  * Find or create a person with smart matching
80
283
  */
@@ -176,12 +379,6 @@ class PeopleModule extends base_1.BaseModule {
176
379
  async deleteAddress(personId, addressId) {
177
380
  return this.deleteResource(`/people/${personId}/addresses/${addressId}`);
178
381
  }
179
- /**
180
- * Get person's social profiles
181
- */
182
- async getSocialProfiles(personId) {
183
- return this.getList(`/people/${personId}/social_profiles`);
184
- }
185
382
  /**
186
383
  * Add a social profile to a person
187
384
  */
@@ -16,9 +16,19 @@ export interface OAuthAuth {
16
16
  refreshToken: string;
17
17
  }) => void | Promise<void>;
18
18
  onRefreshFailure: (error: Error) => void | Promise<void>;
19
+ /** Client ID for token refresh (optional, can use environment variable PCO_APP_ID) */
20
+ clientId?: string;
21
+ /** Client Secret for token refresh (optional, can use environment variable PCO_APP_SECRET) */
22
+ clientSecret?: string;
23
+ }
24
+ /** Authentication configuration for Basic Auth with app credentials */
25
+ export interface BasicAuth {
26
+ type: 'basic';
27
+ appId: string;
28
+ appSecret: string;
19
29
  }
20
30
  /** Union type for authentication configurations */
21
- export type PcoAuthConfig = PersonalAccessTokenAuth | OAuthAuth;
31
+ export type PcoAuthConfig = PersonalAccessTokenAuth | OAuthAuth | BasicAuth;
22
32
  export interface PcoClientConfig {
23
33
  /** Authentication configuration */
24
34
  auth: PcoAuthConfig;
@@ -39,8 +39,14 @@ export interface PersonAttributes extends Attributes {
39
39
  export interface PersonRelationships {
40
40
  emails?: Relationship;
41
41
  phone_numbers?: Relationship;
42
+ addresses?: Relationship;
43
+ household?: Relationship;
42
44
  primary_campus?: Relationship;
43
45
  gender?: Relationship;
46
+ workflow_cards?: Relationship;
47
+ notes?: Relationship;
48
+ field_data?: Relationship;
49
+ social_profiles?: Relationship;
44
50
  }
45
51
  export interface PersonResource extends ResourceObject<'Person', PersonAttributes, PersonRelationships> {
46
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rachelallyson/planning-center-people-ts",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "A strictly typed TypeScript client for Planning Center Online People API with smart matching, batch operations, and enhanced developer experience",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",