@rachelallyson/planning-center-people-ts 2.3.1 → 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,185 @@ 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
+
121
+ ## [2.4.0] - 2025-01-10
122
+
123
+ ### 🎯 **NEW FEATURES - Age Preference Matching & Exact Name Matching**
124
+
125
+ This release introduces intelligent age-based person matching and precise name matching capabilities to enhance person discovery and reduce false positives.
126
+
127
+ ### Added
128
+
129
+ #### **👥 Age Preference Matching**
130
+
131
+ - **🎂 Age-Based Filtering**: New `agePreference` option to prefer adults or children
132
+ - **📅 Age Range Matching**: Support for `minAge` and `maxAge` parameters for precise age targeting
133
+ - **🗓️ Birth Year Matching**: `birthYear` parameter for matching people born in specific years
134
+ - **🧮 Smart Age Calculation**: Enhanced age calculation with timezone-safe date handling
135
+ - **📊 Age-Based Scoring**: Age matching contributes 15% to overall match score for better accuracy
136
+
137
+ #### **🎯 Exact Name Matching**
138
+
139
+ - **✅ Precise Name Matching**: Only matches exact names, eliminating false positives from similar names
140
+ - **🔤 Case-Insensitive**: Maintains case-insensitive matching while ensuring exact character matching
141
+ - **⚡ Performance Optimized**: Simple string comparison for faster matching than fuzzy algorithms
142
+ - **🛡️ Reduced False Positives**: Prevents matching "Jon" when searching for "John"
143
+
144
+ #### **🔧 Enhanced Matching System**
145
+
146
+ - **📈 Improved Scoring Algorithm**: Updated scoring weights for better match prioritization
147
+ - **🎯 Candidate Filtering**: Age-based pre-filtering before scoring for more relevant results
148
+ - **📝 Enhanced Match Reasons**: More descriptive match explanations including age-based reasons
149
+ - **🧪 Comprehensive Testing**: 30+ new test cases covering age preferences and exact name matching
150
+
151
+ ### Usage Examples
152
+
153
+ ```typescript
154
+ // Age preference matching
155
+ const adultPerson = await client.people.findOrCreate({
156
+ firstName: 'Jane',
157
+ lastName: 'Smith',
158
+ agePreference: 'adults', // Prefer 18+ years old
159
+ matchStrategy: 'fuzzy'
160
+ });
161
+
162
+ // Age range matching
163
+ const youngAdult = await client.people.findOrCreate({
164
+ firstName: 'Alice',
165
+ lastName: 'Brown',
166
+ minAge: 20,
167
+ maxAge: 30,
168
+ matchStrategy: 'fuzzy'
169
+ });
170
+
171
+ // Birth year matching
172
+ const millennial = await client.people.findOrCreate({
173
+ firstName: 'David',
174
+ lastName: 'Wilson',
175
+ birthYear: 1990,
176
+ matchStrategy: 'fuzzy'
177
+ });
178
+ ```
179
+
180
+ ### Technical Details
181
+
182
+ - **New Helper Functions**: `calculateAgeSafe()`, `isAdult()`, `isChild()`, `matchesAgeCriteria()`
183
+ - **Enhanced PersonMatchOptions**: Added `agePreference`, `minAge`, `maxAge`, `birthYear` properties
184
+ - **Updated Scoring System**: Age matching now contributes 15% to overall match score
185
+ - **Backward Compatibility**: All existing functionality remains unchanged
186
+
8
187
  ## [2.3.1] - 2025-01-10
9
188
 
10
189
  ### 🐛 **BUG FIXES & STABILITY IMPROVEMENTS**
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
package/dist/helpers.d.ts CHANGED
@@ -15,6 +15,31 @@ export declare function buildQueryParams(params?: {
15
15
  * Calculate age from birthdate string
16
16
  */
17
17
  export declare function calculateAge(birthdate: string): number;
18
+ /**
19
+ * Calculate age from birthdate string, handling invalid dates
20
+ */
21
+ export declare function calculateAgeSafe(birthdate: string | undefined): number | null;
22
+ /**
23
+ * Check if a person is an adult (18+ years old)
24
+ */
25
+ export declare function isAdult(birthdate: string | undefined): boolean;
26
+ /**
27
+ * Check if a person is a child (under 18 years old)
28
+ */
29
+ export declare function isChild(birthdate: string | undefined): boolean;
30
+ /**
31
+ * Check if a person's age matches the given criteria
32
+ */
33
+ export declare function matchesAgeCriteria(birthdate: string | undefined, criteria: {
34
+ agePreference?: 'adults' | 'children' | 'any';
35
+ minAge?: number;
36
+ maxAge?: number;
37
+ birthYear?: number;
38
+ }): boolean;
39
+ /**
40
+ * Calculate birth year from age
41
+ */
42
+ export declare function calculateBirthYearFromAge(age: number): number;
18
43
  /**
19
44
  * Validate email format
20
45
  */
package/dist/helpers.js CHANGED
@@ -2,6 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildQueryParams = buildQueryParams;
4
4
  exports.calculateAge = calculateAge;
5
+ exports.calculateAgeSafe = calculateAgeSafe;
6
+ exports.isAdult = isAdult;
7
+ exports.isChild = isChild;
8
+ exports.matchesAgeCriteria = matchesAgeCriteria;
9
+ exports.calculateBirthYearFromAge = calculateBirthYearFromAge;
5
10
  exports.isValidEmail = isValidEmail;
6
11
  exports.isValidPhone = isValidPhone;
7
12
  exports.formatPersonName = formatPersonName;
@@ -61,6 +66,70 @@ function calculateAge(birthdate) {
61
66
  }
62
67
  return age;
63
68
  }
69
+ /**
70
+ * Calculate age from birthdate string, handling invalid dates
71
+ */
72
+ function calculateAgeSafe(birthdate) {
73
+ if (!birthdate)
74
+ return null;
75
+ try {
76
+ const birth = new Date(birthdate);
77
+ if (isNaN(birth.getTime()))
78
+ return null;
79
+ return calculateAge(birthdate);
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ /**
86
+ * Check if a person is an adult (18+ years old)
87
+ */
88
+ function isAdult(birthdate) {
89
+ const age = calculateAgeSafe(birthdate);
90
+ return age !== null && age >= 18;
91
+ }
92
+ /**
93
+ * Check if a person is a child (under 18 years old)
94
+ */
95
+ function isChild(birthdate) {
96
+ const age = calculateAgeSafe(birthdate);
97
+ return age !== null && age < 18;
98
+ }
99
+ /**
100
+ * Check if a person's age matches the given criteria
101
+ */
102
+ function matchesAgeCriteria(birthdate, criteria) {
103
+ const age = calculateAgeSafe(birthdate);
104
+ // If no birthdate, only match if preference is 'any'
105
+ if (age === null) {
106
+ return criteria.agePreference === 'any' || criteria.agePreference === undefined;
107
+ }
108
+ // Check age preference
109
+ if (criteria.agePreference === 'adults' && age < 18)
110
+ return false;
111
+ if (criteria.agePreference === 'children' && age >= 18)
112
+ return false;
113
+ // Check age range
114
+ if (criteria.minAge !== undefined && age < criteria.minAge)
115
+ return false;
116
+ if (criteria.maxAge !== undefined && age > criteria.maxAge)
117
+ return false;
118
+ // Check birth year
119
+ if (criteria.birthYear !== undefined) {
120
+ const birthYear = new Date(birthdate).getFullYear();
121
+ if (birthYear !== criteria.birthYear)
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ /**
127
+ * Calculate birth year from age
128
+ */
129
+ function calculateBirthYearFromAge(age) {
130
+ const currentYear = new Date().getFullYear();
131
+ return currentYear - age;
132
+ }
64
133
  /**
65
134
  * Validate email format
66
135
  */
@@ -26,6 +26,10 @@ export declare class PersonMatcher {
26
26
  * Get potential matching candidates
27
27
  */
28
28
  private getCandidates;
29
+ /**
30
+ * Filter candidates by age preferences
31
+ */
32
+ private filterByAgePreferences;
29
33
  /**
30
34
  * Create a new person
31
35
  */
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PersonMatcher = void 0;
7
7
  const strategies_1 = require("./strategies");
8
8
  const scoring_1 = require("./scoring");
9
+ const helpers_1 = require("../helpers");
9
10
  class PersonMatcher {
10
11
  constructor(peopleModule) {
11
12
  this.peopleModule = peopleModule;
@@ -102,7 +103,30 @@ class PersonMatcher {
102
103
  }
103
104
  // Remove duplicates based on person ID
104
105
  const uniqueCandidates = candidates.filter((person, index, self) => index === self.findIndex(p => p.id === person.id));
105
- return uniqueCandidates;
106
+ // Filter by age preferences if specified
107
+ const ageFilteredCandidates = this.filterByAgePreferences(uniqueCandidates, options);
108
+ return ageFilteredCandidates;
109
+ }
110
+ /**
111
+ * Filter candidates by age preferences
112
+ */
113
+ filterByAgePreferences(candidates, options) {
114
+ // If no age criteria specified, return all candidates
115
+ if (!options.agePreference &&
116
+ options.minAge === undefined &&
117
+ options.maxAge === undefined &&
118
+ options.birthYear === undefined) {
119
+ return candidates;
120
+ }
121
+ return candidates.filter(person => {
122
+ const birthdate = person.attributes?.birthdate;
123
+ return (0, helpers_1.matchesAgeCriteria)(birthdate, {
124
+ agePreference: options.agePreference,
125
+ minAge: options.minAge,
126
+ maxAge: options.maxAge,
127
+ birthYear: options.birthYear
128
+ });
129
+ });
106
130
  }
107
131
  /**
108
132
  * Create a new person
@@ -21,9 +21,13 @@ export declare class MatchScorer {
21
21
  */
22
22
  private scorePhoneMatch;
23
23
  /**
24
- * Score name matching
24
+ * Score name matching - only exact matches
25
25
  */
26
26
  private scoreNameMatch;
27
+ /**
28
+ * Score age matching
29
+ */
30
+ private scoreAgeMatch;
27
31
  /**
28
32
  * Score additional criteria
29
33
  */
@@ -4,6 +4,7 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MatchScorer = void 0;
7
+ const helpers_1 = require("../helpers");
7
8
  class MatchScorer {
8
9
  /**
9
10
  * Score a person match based on various criteria
@@ -14,14 +15,14 @@ class MatchScorer {
14
15
  // Email matching (highest weight)
15
16
  if (options.email) {
16
17
  const emailScore = this.scoreEmailMatch(person, options.email);
17
- totalScore += emailScore * 0.4;
18
- maxScore += 0.4;
18
+ totalScore += emailScore * 0.35;
19
+ maxScore += 0.35;
19
20
  }
20
21
  // Phone matching (high weight)
21
22
  if (options.phone) {
22
23
  const phoneScore = this.scorePhoneMatch(person, options.phone);
23
- totalScore += phoneScore * 0.3;
24
- maxScore += 0.3;
24
+ totalScore += phoneScore * 0.25;
25
+ maxScore += 0.25;
25
26
  }
26
27
  // Name matching (medium weight)
27
28
  if (options.firstName || options.lastName) {
@@ -29,10 +30,14 @@ class MatchScorer {
29
30
  totalScore += nameScore * 0.2;
30
31
  maxScore += 0.2;
31
32
  }
33
+ // Age matching (medium weight)
34
+ const ageScore = this.scoreAgeMatch(person, options);
35
+ totalScore += ageScore * 0.15;
36
+ maxScore += 0.15;
32
37
  // Additional criteria (lower weight)
33
38
  const additionalScore = this.scoreAdditionalCriteria(person, options);
34
- totalScore += additionalScore * 0.1;
35
- maxScore += 0.1;
39
+ totalScore += additionalScore * 0.05;
40
+ maxScore += 0.05;
36
41
  return maxScore > 0 ? totalScore / maxScore : 0;
37
42
  }
38
43
  /**
@@ -51,8 +56,24 @@ class MatchScorer {
51
56
  if (nameScore > 0.8) {
52
57
  reasons.push('exact name match');
53
58
  }
54
- else if (nameScore > 0.6) {
55
- reasons.push('similar name match');
59
+ else if (nameScore > 0) {
60
+ reasons.push('partial name match');
61
+ }
62
+ }
63
+ // Add age-based match reasons
64
+ const ageScore = this.scoreAgeMatch(person, options);
65
+ if (ageScore > 0.8) {
66
+ const age = (0, helpers_1.calculateAgeSafe)(person.attributes?.birthdate);
67
+ if (age !== null) {
68
+ if (options.agePreference === 'adults') {
69
+ reasons.push('adult age match');
70
+ }
71
+ else if (options.agePreference === 'children') {
72
+ reasons.push('child age match');
73
+ }
74
+ else {
75
+ reasons.push(`age ${age} match`);
76
+ }
56
77
  }
57
78
  }
58
79
  if (reasons.length === 0) {
@@ -79,25 +100,72 @@ class MatchScorer {
79
100
  return 0;
80
101
  }
81
102
  /**
82
- * Score name matching
103
+ * Score name matching - only exact matches
83
104
  */
84
105
  scoreNameMatch(person, options) {
85
106
  const attrs = person.attributes;
86
107
  if (!attrs)
87
108
  return 0;
88
109
  let score = 0;
89
- // First name matching
110
+ // First name matching - exact match only
90
111
  if (options.firstName && attrs.first_name) {
91
- const firstNameScore = this.calculateStringSimilarity(options.firstName.toLowerCase(), attrs.first_name.toLowerCase());
92
- score += firstNameScore * 0.5;
112
+ const firstNameMatch = options.firstName.toLowerCase() === attrs.first_name.toLowerCase();
113
+ score += firstNameMatch ? 0.5 : 0;
93
114
  }
94
- // Last name matching
115
+ // Last name matching - exact match only
95
116
  if (options.lastName && attrs.last_name) {
96
- const lastNameScore = this.calculateStringSimilarity(options.lastName.toLowerCase(), attrs.last_name.toLowerCase());
97
- score += lastNameScore * 0.5;
117
+ const lastNameMatch = options.lastName.toLowerCase() === attrs.last_name.toLowerCase();
118
+ score += lastNameMatch ? 0.5 : 0;
98
119
  }
99
120
  return score;
100
121
  }
122
+ /**
123
+ * Score age matching
124
+ */
125
+ scoreAgeMatch(person, options) {
126
+ const birthdate = person.attributes?.birthdate;
127
+ // If no age criteria specified, return neutral score
128
+ if (!options.agePreference &&
129
+ options.minAge === undefined &&
130
+ options.maxAge === undefined &&
131
+ options.birthYear === undefined) {
132
+ return 0.5; // Neutral score
133
+ }
134
+ // If no birthdate available, return low score
135
+ if (!birthdate) {
136
+ return 0.1;
137
+ }
138
+ // Check if person matches age criteria
139
+ const matches = (0, helpers_1.matchesAgeCriteria)(birthdate, {
140
+ agePreference: options.agePreference,
141
+ minAge: options.minAge,
142
+ maxAge: options.maxAge,
143
+ birthYear: options.birthYear
144
+ });
145
+ if (!matches) {
146
+ return 0; // No match
147
+ }
148
+ // Calculate bonus score based on how well the age matches
149
+ const age = (0, helpers_1.calculateAgeSafe)(birthdate);
150
+ if (age === null)
151
+ return 0.5;
152
+ let bonusScore = 0;
153
+ // Bonus for exact age range match
154
+ if (options.minAge !== undefined && options.maxAge !== undefined) {
155
+ if (age >= options.minAge && age <= options.maxAge) {
156
+ bonusScore += 0.3;
157
+ }
158
+ }
159
+ // Bonus for exact birth year match
160
+ if (options.birthYear !== undefined) {
161
+ const birthYear = new Date(birthdate).getFullYear();
162
+ if (birthYear === options.birthYear) {
163
+ bonusScore += 0.4;
164
+ }
165
+ }
166
+ // Base score for matching criteria
167
+ return Math.min(0.6 + bonusScore, 1.0);
168
+ }
101
169
  /**
102
170
  * Score additional criteria
103
171
  */
@@ -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[];
@@ -50,6 +50,10 @@ export interface PersonMatchOptions {
50
50
  matchStrategy?: 'exact' | 'fuzzy' | 'aggressive';
51
51
  campus?: string;
52
52
  createIfNotFound?: boolean;
53
+ agePreference?: 'adults' | 'children' | 'any';
54
+ minAge?: number;
55
+ maxAge?: number;
56
+ birthYear?: number;
53
57
  }
54
58
  export declare class PeopleModule extends BaseModule {
55
59
  private personMatcher;
@@ -82,6 +86,94 @@ export declare class PeopleModule extends BaseModule {
82
86
  * Delete a person
83
87
  */
84
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
+ }>;
85
177
  /**
86
178
  * Find or create a person with smart matching
87
179
  */
@@ -160,14 +252,6 @@ export declare class PeopleModule extends BaseModule {
160
252
  * Delete a person's address
161
253
  */
162
254
  deleteAddress(personId: string, addressId: string): Promise<void>;
163
- /**
164
- * Get person's social profiles
165
- */
166
- getSocialProfiles(personId: string): Promise<{
167
- data: SocialProfileResource[];
168
- meta?: any;
169
- links?: any;
170
- }>;
171
255
  /**
172
256
  * Add a social profile to a person
173
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.3.1",
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",