@rachelallyson/planning-center-people-ts 2.14.1 → 3.0.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 +76 -7
- package/README.md +42 -4
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +14 -6
- package/dist/client.d.ts +33 -8
- package/dist/client.js +47 -22
- package/dist/core.d.ts +4 -2
- package/dist/core.js +3 -2
- package/dist/error-handling.d.ts +4 -4
- package/dist/error-handling.js +13 -2
- package/dist/error-scenarios.d.ts +11 -7
- package/dist/error-scenarios.js +26 -10
- package/dist/helpers.d.ts +124 -48
- package/dist/helpers.js +236 -93
- package/dist/index.d.ts +9 -7
- package/dist/index.js +31 -72
- package/dist/matching/matcher.d.ts +8 -4
- package/dist/matching/matcher.js +51 -58
- package/dist/matching/scoring.d.ts +9 -6
- package/dist/matching/scoring.js +18 -14
- package/dist/modules/campus.d.ts +31 -36
- package/dist/modules/campus.js +36 -49
- package/dist/modules/contacts.d.ts +33 -29
- package/dist/modules/contacts.js +36 -12
- package/dist/modules/fields.d.ts +39 -55
- package/dist/modules/fields.js +65 -105
- package/dist/modules/forms.d.ts +35 -24
- package/dist/modules/forms.js +41 -23
- package/dist/modules/households.d.ts +17 -19
- package/dist/modules/households.js +25 -34
- package/dist/modules/lists.d.ts +30 -28
- package/dist/modules/lists.js +55 -42
- package/dist/modules/notes.d.ts +32 -30
- package/dist/modules/notes.js +40 -52
- package/dist/modules/people.d.ts +83 -71
- package/dist/modules/people.js +323 -172
- package/dist/modules/reports.d.ts +18 -32
- package/dist/modules/reports.js +28 -40
- package/dist/modules/service-time.d.ts +19 -24
- package/dist/modules/service-time.js +28 -28
- package/dist/modules/workflows.d.ts +42 -47
- package/dist/modules/workflows.js +50 -53
- package/dist/performance.d.ts +14 -10
- package/dist/performance.js +61 -25
- package/dist/testing/recorder.js +11 -2
- package/dist/testing/simple-builders.d.ts +6 -4
- package/dist/testing/simple-builders.js +36 -49
- package/dist/testing/types.d.ts +4 -0
- package/dist/types/api-options.d.ts +380 -0
- package/dist/types/api-options.js +6 -0
- package/dist/types/client.d.ts +4 -2
- package/dist/types/client.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/people.d.ts +47 -9
- package/package.json +7 -7
- package/dist/core/http.d.ts +0 -56
- package/dist/core/http.js +0 -360
- package/dist/core/pagination.d.ts +0 -34
- package/dist/core/pagination.js +0 -178
- package/dist/people/contacts.d.ts +0 -43
- package/dist/people/contacts.js +0 -122
- package/dist/people/core.d.ts +0 -28
- package/dist/people/core.js +0 -69
- package/dist/people/fields.d.ts +0 -68
- package/dist/people/fields.js +0 -305
- package/dist/people/households.d.ts +0 -15
- package/dist/people/households.js +0 -31
- package/dist/people/index.d.ts +0 -8
- package/dist/people/index.js +0 -25
- package/dist/people/lists.d.ts +0 -34
- package/dist/people/lists.js +0 -48
- package/dist/people/notes.d.ts +0 -30
- package/dist/people/notes.js +0 -37
- package/dist/people/organization.d.ts +0 -12
- package/dist/people/organization.js +0 -15
- package/dist/people/workflows.d.ts +0 -37
- package/dist/people/workflows.js +0 -75
package/dist/core/http.js
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* v2.0.0 HTTP Client
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PcoHttpClient = void 0;
|
|
7
|
-
const planning_center_base_ts_1 = require("@rachelallyson/planning-center-base-ts");
|
|
8
|
-
const planning_center_base_ts_2 = require("@rachelallyson/planning-center-base-ts");
|
|
9
|
-
const planning_center_base_ts_3 = require("@rachelallyson/planning-center-base-ts");
|
|
10
|
-
class PcoHttpClient {
|
|
11
|
-
constructor(config, eventEmitter) {
|
|
12
|
-
this.config = config;
|
|
13
|
-
this.eventEmitter = eventEmitter;
|
|
14
|
-
this.requestIdGenerator = new planning_center_base_ts_1.RequestIdGenerator();
|
|
15
|
-
this.performanceMetrics = new planning_center_base_ts_1.PerformanceMetrics();
|
|
16
|
-
this.rateLimitTracker = new planning_center_base_ts_1.RateLimitTracker();
|
|
17
|
-
// Initialize rate limiter
|
|
18
|
-
this.rateLimiter = new planning_center_base_ts_2.PcoRateLimiter(100, 20000); // 100 requests per 20 seconds
|
|
19
|
-
}
|
|
20
|
-
async request(options) {
|
|
21
|
-
const requestId = this.requestIdGenerator.generate();
|
|
22
|
-
const startTime = Date.now();
|
|
23
|
-
// Emit request start event
|
|
24
|
-
this.eventEmitter.emit({
|
|
25
|
-
type: 'request:start',
|
|
26
|
-
endpoint: options.endpoint,
|
|
27
|
-
method: options.method,
|
|
28
|
-
requestId,
|
|
29
|
-
timestamp: new Date().toISOString(),
|
|
30
|
-
});
|
|
31
|
-
try {
|
|
32
|
-
// Wait for rate limiter
|
|
33
|
-
await this.rateLimiter.waitForAvailability();
|
|
34
|
-
const response = await this.makeRequest(options, requestId);
|
|
35
|
-
const duration = Date.now() - startTime;
|
|
36
|
-
// Record performance metrics
|
|
37
|
-
this.performanceMetrics.record(`${options.method} ${options.endpoint}`, duration, true);
|
|
38
|
-
// Update rate limit tracking
|
|
39
|
-
this.updateRateLimitTracking(options.endpoint, response.headers);
|
|
40
|
-
// Emit request complete event
|
|
41
|
-
this.eventEmitter.emit({
|
|
42
|
-
type: 'request:complete',
|
|
43
|
-
endpoint: options.endpoint,
|
|
44
|
-
method: options.method,
|
|
45
|
-
status: response.status,
|
|
46
|
-
duration,
|
|
47
|
-
requestId,
|
|
48
|
-
timestamp: new Date().toISOString(),
|
|
49
|
-
});
|
|
50
|
-
return response;
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
const duration = Date.now() - startTime;
|
|
54
|
-
// Record performance metrics
|
|
55
|
-
this.performanceMetrics.record(`${options.method} ${options.endpoint}`, duration, false);
|
|
56
|
-
// Emit request error event
|
|
57
|
-
this.eventEmitter.emit({
|
|
58
|
-
type: 'request:error',
|
|
59
|
-
endpoint: options.endpoint,
|
|
60
|
-
method: options.method,
|
|
61
|
-
error: error,
|
|
62
|
-
requestId,
|
|
63
|
-
timestamp: new Date().toISOString(),
|
|
64
|
-
});
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
async makeRequest(options, requestId, retryCount = 0) {
|
|
69
|
-
const baseURL = this.config.baseURL || 'https://api.planningcenteronline.com/people/v2';
|
|
70
|
-
let url = options.endpoint.startsWith('http') ? options.endpoint : `${baseURL}${options.endpoint}`;
|
|
71
|
-
// Add query parameters
|
|
72
|
-
if (options.params) {
|
|
73
|
-
const searchParams = new URLSearchParams();
|
|
74
|
-
Object.entries(options.params).forEach(([key, value]) => {
|
|
75
|
-
if (value !== undefined && value !== null) {
|
|
76
|
-
searchParams.append(key, String(value));
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
const queryString = searchParams.toString();
|
|
80
|
-
if (queryString) {
|
|
81
|
-
url += url.includes('?') ? `&${queryString}` : `?${queryString}`;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// Prepare headers
|
|
85
|
-
const headers = {
|
|
86
|
-
'Accept': 'application/json',
|
|
87
|
-
'Content-Type': 'application/json',
|
|
88
|
-
...this.config.headers,
|
|
89
|
-
...options.headers,
|
|
90
|
-
};
|
|
91
|
-
// Add authentication
|
|
92
|
-
this.addAuthentication(headers);
|
|
93
|
-
// Prepare request options
|
|
94
|
-
const requestOptions = {
|
|
95
|
-
headers,
|
|
96
|
-
method: options.method,
|
|
97
|
-
};
|
|
98
|
-
// Add body for POST/PATCH requests
|
|
99
|
-
if ((options.method === 'POST' || options.method === 'PATCH') && options.data) {
|
|
100
|
-
// Determine resource type from endpoint
|
|
101
|
-
const resourceType = this.getResourceTypeFromEndpoint(options.endpoint);
|
|
102
|
-
// Separate attributes and relationships
|
|
103
|
-
const { relationships, ...attributes } = options.data;
|
|
104
|
-
const jsonApiData = {
|
|
105
|
-
data: {
|
|
106
|
-
type: resourceType,
|
|
107
|
-
attributes
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
// Add relationships if present
|
|
111
|
-
if (relationships) {
|
|
112
|
-
jsonApiData.data.relationships = relationships;
|
|
113
|
-
}
|
|
114
|
-
requestOptions.body = JSON.stringify(jsonApiData);
|
|
115
|
-
}
|
|
116
|
-
// Add timeout
|
|
117
|
-
const timeout = options.timeout || this.config.timeout || 30000;
|
|
118
|
-
const controller = new AbortController();
|
|
119
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
120
|
-
requestOptions.signal = controller.signal;
|
|
121
|
-
try {
|
|
122
|
-
const response = await fetch(url, requestOptions);
|
|
123
|
-
clearTimeout(timeoutId);
|
|
124
|
-
// Update rate limiter from headers
|
|
125
|
-
const rateLimitHeaders = {
|
|
126
|
-
'Retry-After': response.headers.get('retry-after') || undefined,
|
|
127
|
-
'X-PCO-API-Request-Rate-Count': response.headers.get('x-pco-api-request-rate-count') || undefined,
|
|
128
|
-
'X-PCO-API-Request-Rate-Limit': response.headers.get('x-pco-api-request-rate-limit') || undefined,
|
|
129
|
-
'X-PCO-API-Request-Rate-Period': response.headers.get('x-pco-api-request-rate-period') || undefined,
|
|
130
|
-
};
|
|
131
|
-
this.rateLimiter.updateFromHeaders(rateLimitHeaders);
|
|
132
|
-
this.rateLimiter.recordRequest();
|
|
133
|
-
// Handle 429 responses
|
|
134
|
-
if (response.status === 429) {
|
|
135
|
-
if (retryCount >= 5) {
|
|
136
|
-
throw new Error(`Rate limit exceeded after ${retryCount} retries`);
|
|
137
|
-
}
|
|
138
|
-
await this.rateLimiter.waitForAvailability();
|
|
139
|
-
return this.makeRequest(options, requestId, retryCount + 1);
|
|
140
|
-
}
|
|
141
|
-
// Handle other errors
|
|
142
|
-
if (!response.ok) {
|
|
143
|
-
// Handle 401 errors with token refresh if available
|
|
144
|
-
if (response.status === 401 && this.config.auth.type === 'oauth') {
|
|
145
|
-
if (retryCount >= 3) {
|
|
146
|
-
throw new Error(`Authentication failed after ${retryCount} retries`);
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
await this.attemptTokenRefresh();
|
|
150
|
-
return this.makeRequest(options, requestId, retryCount + 1);
|
|
151
|
-
}
|
|
152
|
-
catch (refreshError) {
|
|
153
|
-
console.warn('Token refresh failed:', refreshError);
|
|
154
|
-
// Call the onRefreshFailure callback
|
|
155
|
-
await this.config.auth.onRefreshFailure?.(refreshError);
|
|
156
|
-
throw refreshError;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
let errorData;
|
|
160
|
-
try {
|
|
161
|
-
errorData = await response.json();
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
errorData = {};
|
|
165
|
-
}
|
|
166
|
-
throw planning_center_base_ts_3.PcoApiError.fromFetchError(response, errorData);
|
|
167
|
-
}
|
|
168
|
-
// Parse response
|
|
169
|
-
if (options.method === 'DELETE') {
|
|
170
|
-
return {
|
|
171
|
-
data: undefined,
|
|
172
|
-
status: response.status,
|
|
173
|
-
headers: this.extractHeaders(response),
|
|
174
|
-
requestId,
|
|
175
|
-
duration: 0, // Will be set by caller
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
const data = await response.json();
|
|
179
|
-
return {
|
|
180
|
-
data,
|
|
181
|
-
status: response.status,
|
|
182
|
-
headers: this.extractHeaders(response),
|
|
183
|
-
requestId,
|
|
184
|
-
duration: 0, // Will be set by caller
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
clearTimeout(timeoutId);
|
|
189
|
-
// Handle timeout/abort errors
|
|
190
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
191
|
-
throw new Error(`Request timeout after ${timeout}ms`);
|
|
192
|
-
}
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
addAuthentication(headers) {
|
|
197
|
-
if (this.config.auth.type === 'personal_access_token') {
|
|
198
|
-
// Personal Access Tokens use client_id:secret format with HTTP Basic Auth
|
|
199
|
-
// Get client ID from config (required)
|
|
200
|
-
const clientId = this.config.auth.personalAccessToken;
|
|
201
|
-
// Get client secret from config or environment (with config taking precedence)
|
|
202
|
-
const clientSecret = this.config.auth.personalAccessTokenSecret ||
|
|
203
|
-
process.env.PCO_PERSONAL_ACCESS_SECRET;
|
|
204
|
-
if (!clientId) {
|
|
205
|
-
throw new Error('personalAccessToken is required for personal access token authentication');
|
|
206
|
-
}
|
|
207
|
-
if (!clientSecret) {
|
|
208
|
-
throw new Error('personalAccessTokenSecret (in config) or PCO_PERSONAL_ACCESS_SECRET environment variable is required for personal access token authentication');
|
|
209
|
-
}
|
|
210
|
-
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
211
|
-
headers.Authorization = `Basic ${credentials}`;
|
|
212
|
-
}
|
|
213
|
-
else if (this.config.auth.type === 'oauth') {
|
|
214
|
-
headers.Authorization = `Bearer ${this.config.auth.accessToken}`;
|
|
215
|
-
}
|
|
216
|
-
else if (this.config.auth.type === 'basic') {
|
|
217
|
-
// Basic auth with app credentials
|
|
218
|
-
const credentials = Buffer.from(`${this.config.auth.appId}:${this.config.auth.appSecret}`).toString('base64');
|
|
219
|
-
headers.Authorization = `Basic ${credentials}`;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
getResourceTypeFromEndpoint(endpoint) {
|
|
223
|
-
// Extract resource type from endpoint
|
|
224
|
-
// /households -> Household
|
|
225
|
-
// /people -> Person
|
|
226
|
-
// /emails -> Email
|
|
227
|
-
// etc.
|
|
228
|
-
const pathParts = endpoint.split('/').filter(part => part.length > 0);
|
|
229
|
-
const resourcePath = pathParts[pathParts.length - 1];
|
|
230
|
-
// Convert kebab-case to PascalCase and make singular
|
|
231
|
-
const pascalCase = resourcePath
|
|
232
|
-
.split('-')
|
|
233
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
234
|
-
.join('');
|
|
235
|
-
// Make singular (remove trailing 's' if it exists and the word is longer than 3 characters)
|
|
236
|
-
if (pascalCase.endsWith('s') && pascalCase.length > 3) {
|
|
237
|
-
return pascalCase.slice(0, -1);
|
|
238
|
-
}
|
|
239
|
-
return pascalCase;
|
|
240
|
-
}
|
|
241
|
-
extractHeaders(response) {
|
|
242
|
-
const headers = {};
|
|
243
|
-
response.headers.forEach((value, key) => {
|
|
244
|
-
headers[key] = value;
|
|
245
|
-
});
|
|
246
|
-
return headers;
|
|
247
|
-
}
|
|
248
|
-
async attemptTokenRefresh() {
|
|
249
|
-
if (this.config.auth.type !== 'oauth') {
|
|
250
|
-
throw new Error('Token refresh is only available for OAuth authentication');
|
|
251
|
-
}
|
|
252
|
-
const baseURL = this.config.baseURL || 'https://api.planningcenteronline.com/people/v2';
|
|
253
|
-
const tokenUrl = baseURL.replace('/people/v2', '/oauth/token');
|
|
254
|
-
// Prepare the request body for token refresh
|
|
255
|
-
const body = new URLSearchParams({
|
|
256
|
-
grant_type: 'refresh_token',
|
|
257
|
-
refresh_token: this.config.auth.refreshToken || '',
|
|
258
|
-
});
|
|
259
|
-
// Add client credentials if available from the config or environment
|
|
260
|
-
const clientId = this.config.auth.appId || process.env.PCO_APP_ID;
|
|
261
|
-
const clientSecret = this.config.auth.appSecret || process.env.PCO_APP_SECRET;
|
|
262
|
-
if (clientId && clientSecret) {
|
|
263
|
-
body.append('client_id', clientId);
|
|
264
|
-
body.append('client_secret', clientSecret);
|
|
265
|
-
}
|
|
266
|
-
const response = await fetch(tokenUrl, {
|
|
267
|
-
method: 'POST',
|
|
268
|
-
headers: {
|
|
269
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
270
|
-
'Accept': 'application/json',
|
|
271
|
-
},
|
|
272
|
-
body: body.toString(),
|
|
273
|
-
});
|
|
274
|
-
if (!response.ok) {
|
|
275
|
-
const errorData = await response.json().catch(() => ({}));
|
|
276
|
-
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}. ${JSON.stringify(errorData)}`);
|
|
277
|
-
}
|
|
278
|
-
const tokens = await response.json();
|
|
279
|
-
// Update the config with new tokens
|
|
280
|
-
this.config.auth.accessToken = tokens.access_token;
|
|
281
|
-
this.config.auth.refreshToken = tokens.refresh_token || this.config.auth.refreshToken;
|
|
282
|
-
// Call the onRefresh callback
|
|
283
|
-
await this.config.auth.onRefresh?.({
|
|
284
|
-
accessToken: tokens.access_token,
|
|
285
|
-
refreshToken: tokens.refresh_token || this.config.auth.refreshToken
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
updateRateLimitTracking(endpoint, headers) {
|
|
289
|
-
const limit = headers['x-pco-api-request-rate-limit'];
|
|
290
|
-
const remaining = headers['x-pco-api-request-rate-count'];
|
|
291
|
-
const resetTime = headers['retry-after'];
|
|
292
|
-
if (limit && remaining && resetTime) {
|
|
293
|
-
this.rateLimitTracker.update(endpoint, parseInt(limit), parseInt(remaining), Date.now() + parseInt(resetTime) * 1000);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
getPerformanceMetrics() {
|
|
297
|
-
return this.performanceMetrics.getMetrics();
|
|
298
|
-
}
|
|
299
|
-
getRateLimitInfo() {
|
|
300
|
-
return this.rateLimitTracker.getAllLimits();
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Get authentication header for external services (like file uploads)
|
|
304
|
-
*/
|
|
305
|
-
getAuthHeader() {
|
|
306
|
-
// The base package's PcoHttpClient handles authentication, so this method should delegate
|
|
307
|
-
// But for backward compatibility, we'll implement it
|
|
308
|
-
if (this.config.auth.type === 'personal_access_token') {
|
|
309
|
-
const clientSecret = this.config.auth.personalAccessTokenSecret || process.env.PCO_PERSONAL_ACCESS_SECRET;
|
|
310
|
-
return `Basic ${Buffer.from(`${this.config.auth.personalAccessToken}:${clientSecret}`).toString('base64')}`;
|
|
311
|
-
}
|
|
312
|
-
else if (this.config.auth.type === 'oauth') {
|
|
313
|
-
return `Bearer ${this.config.auth.accessToken}`;
|
|
314
|
-
}
|
|
315
|
-
else if (this.config.auth.type === 'basic') {
|
|
316
|
-
return `Basic ${Buffer.from(`${this.config.auth.appId}:${this.config.auth.appSecret}`).toString('base64')}`;
|
|
317
|
-
}
|
|
318
|
-
return '';
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Make HTTPS request using Node.js HTTPS module (fallback when fetch is unavailable)
|
|
322
|
-
*/
|
|
323
|
-
async makeHttpsRequest(url, options) {
|
|
324
|
-
const https = require('https');
|
|
325
|
-
const urlObj = new URL(url);
|
|
326
|
-
const requestOptions = {
|
|
327
|
-
hostname: urlObj.hostname,
|
|
328
|
-
port: urlObj.port || 443,
|
|
329
|
-
path: urlObj.pathname + urlObj.search,
|
|
330
|
-
method: options.method || 'GET',
|
|
331
|
-
headers: options.headers,
|
|
332
|
-
};
|
|
333
|
-
return new Promise((resolve, reject) => {
|
|
334
|
-
const req = https.request(requestOptions, (res) => {
|
|
335
|
-
let data = '';
|
|
336
|
-
res.on('data', (chunk) => {
|
|
337
|
-
data += chunk;
|
|
338
|
-
});
|
|
339
|
-
res.on('end', () => {
|
|
340
|
-
// Create a response-like object
|
|
341
|
-
const response = {
|
|
342
|
-
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
343
|
-
status: res.statusCode,
|
|
344
|
-
statusText: res.statusMessage,
|
|
345
|
-
headers: res.headers,
|
|
346
|
-
text: () => Promise.resolve(data),
|
|
347
|
-
json: () => Promise.resolve(JSON.parse(data)),
|
|
348
|
-
};
|
|
349
|
-
resolve(response);
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
req.on('error', reject);
|
|
353
|
-
if (options.body) {
|
|
354
|
-
req.write(options.body);
|
|
355
|
-
}
|
|
356
|
-
req.end();
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
exports.PcoHttpClient = PcoHttpClient;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2.0.0 Pagination Utilities
|
|
3
|
-
*/
|
|
4
|
-
import type { ResourceObject, Paginated } from '../types';
|
|
5
|
-
import type { PcoHttpClient } from './http';
|
|
6
|
-
export interface PaginationOptions {
|
|
7
|
-
/** Maximum number of pages to fetch */
|
|
8
|
-
maxPages?: number;
|
|
9
|
-
/** Items per page */
|
|
10
|
-
perPage?: number;
|
|
11
|
-
/** Progress callback */
|
|
12
|
-
onProgress?: (current: number, total: number) => void;
|
|
13
|
-
/** Delay between requests in milliseconds */
|
|
14
|
-
delay?: number;
|
|
15
|
-
}
|
|
16
|
-
export interface PaginationResult<T> {
|
|
17
|
-
data: T[];
|
|
18
|
-
totalCount: number;
|
|
19
|
-
pagesFetched: number;
|
|
20
|
-
duration: number;
|
|
21
|
-
}
|
|
22
|
-
export declare class PaginationHelper {
|
|
23
|
-
private httpClient;
|
|
24
|
-
constructor(httpClient: PcoHttpClient);
|
|
25
|
-
getAllPages<T extends ResourceObject<string, any, any>>(endpoint: string, params?: Record<string, any>, options?: PaginationOptions): Promise<PaginationResult<T>>;
|
|
26
|
-
getPage<T extends ResourceObject<string, any, any>>(endpoint: string, page?: number, perPage?: number, params?: Record<string, any>): Promise<Paginated<T>>;
|
|
27
|
-
streamPages<T extends ResourceObject<string, any, any>>(endpoint: string, params?: Record<string, any>, options?: PaginationOptions): AsyncGenerator<T[], void, unknown>;
|
|
28
|
-
/**
|
|
29
|
-
* Get all pages with parallel processing for better performance
|
|
30
|
-
*/
|
|
31
|
-
getAllPagesParallel<T extends ResourceObject<string, any, any>>(endpoint: string, params?: Record<string, any>, options?: PaginationOptions & {
|
|
32
|
-
maxConcurrency?: number;
|
|
33
|
-
}): Promise<PaginationResult<T>>;
|
|
34
|
-
}
|
package/dist/core/pagination.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* v2.0.0 Pagination Utilities
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PaginationHelper = void 0;
|
|
7
|
-
class PaginationHelper {
|
|
8
|
-
constructor(httpClient) {
|
|
9
|
-
this.httpClient = httpClient;
|
|
10
|
-
}
|
|
11
|
-
async getAllPages(endpoint, params = {}, options = {}) {
|
|
12
|
-
// Ensure endpoint is a string
|
|
13
|
-
if (typeof endpoint !== 'string') {
|
|
14
|
-
throw new Error(`Expected endpoint to be a string, got ${typeof endpoint}`);
|
|
15
|
-
}
|
|
16
|
-
const { maxPages = 1000, perPage = 100, onProgress, delay = 50, } = options;
|
|
17
|
-
const startTime = Date.now();
|
|
18
|
-
const allData = [];
|
|
19
|
-
let page = 1;
|
|
20
|
-
let hasMore = true;
|
|
21
|
-
let totalCount = 0;
|
|
22
|
-
while (hasMore && page <= maxPages) {
|
|
23
|
-
const response = await this.httpClient.request({
|
|
24
|
-
method: 'GET',
|
|
25
|
-
endpoint,
|
|
26
|
-
params: {
|
|
27
|
-
...params,
|
|
28
|
-
page,
|
|
29
|
-
per_page: perPage,
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
if (response.data.data && Array.isArray(response.data.data)) {
|
|
33
|
-
allData.push(...response.data.data);
|
|
34
|
-
}
|
|
35
|
-
if (response.data.meta?.total_count) {
|
|
36
|
-
totalCount = Number(response.data.meta.total_count) || 0;
|
|
37
|
-
}
|
|
38
|
-
// Check if we have a next link and if it's different from current page
|
|
39
|
-
const nextLink = response.data.links?.next;
|
|
40
|
-
hasMore = !!nextLink;
|
|
41
|
-
// Additional safeguard: if we're getting the same page repeatedly, break the loop
|
|
42
|
-
if (hasMore && nextLink && nextLink.includes(`page=${page}`)) {
|
|
43
|
-
console.warn(`Pagination loop detected: next link points to same page ${page}. Breaking loop.`);
|
|
44
|
-
hasMore = false;
|
|
45
|
-
}
|
|
46
|
-
page++;
|
|
47
|
-
if (onProgress) {
|
|
48
|
-
onProgress(allData.length, totalCount || allData.length);
|
|
49
|
-
}
|
|
50
|
-
// Add delay between requests to respect rate limits
|
|
51
|
-
if (hasMore && delay > 0) {
|
|
52
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
data: allData,
|
|
57
|
-
totalCount,
|
|
58
|
-
pagesFetched: page - 1,
|
|
59
|
-
duration: Date.now() - startTime,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
async getPage(endpoint, page = 1, perPage = 100, params = {}) {
|
|
63
|
-
const response = await this.httpClient.request({
|
|
64
|
-
method: 'GET',
|
|
65
|
-
endpoint,
|
|
66
|
-
params: {
|
|
67
|
-
...params,
|
|
68
|
-
page,
|
|
69
|
-
per_page: perPage,
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
return response.data;
|
|
73
|
-
}
|
|
74
|
-
async *streamPages(endpoint, params = {}, options = {}) {
|
|
75
|
-
const { maxPages = 1000, perPage = 100, delay = 50, } = options;
|
|
76
|
-
let page = 1;
|
|
77
|
-
let hasMore = true;
|
|
78
|
-
while (hasMore && page <= maxPages) {
|
|
79
|
-
const response = await this.httpClient.request({
|
|
80
|
-
method: 'GET',
|
|
81
|
-
endpoint,
|
|
82
|
-
params: {
|
|
83
|
-
...params,
|
|
84
|
-
page,
|
|
85
|
-
per_page: perPage,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
if (response.data.data && Array.isArray(response.data.data)) {
|
|
89
|
-
yield response.data.data;
|
|
90
|
-
}
|
|
91
|
-
// Check if we have a next link and if it's different from current page
|
|
92
|
-
const nextLink = response.data.links?.next;
|
|
93
|
-
hasMore = !!nextLink;
|
|
94
|
-
// Additional safeguard: if we're getting the same page repeatedly, break the loop
|
|
95
|
-
if (hasMore && nextLink && nextLink.includes(`page=${page}`)) {
|
|
96
|
-
console.warn(`Pagination loop detected: next link points to same page ${page}. Breaking loop.`);
|
|
97
|
-
hasMore = false;
|
|
98
|
-
}
|
|
99
|
-
page++;
|
|
100
|
-
if (hasMore && delay > 0) {
|
|
101
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Get all pages with parallel processing for better performance
|
|
107
|
-
*/
|
|
108
|
-
async getAllPagesParallel(endpoint, params = {}, options = {}) {
|
|
109
|
-
const { maxPages = 1000, perPage = 100, maxConcurrency = 3, onProgress, } = options;
|
|
110
|
-
const startTime = Date.now();
|
|
111
|
-
// First, get the first page to determine total count
|
|
112
|
-
const firstPage = await this.getPage(endpoint, 1, perPage, params);
|
|
113
|
-
const totalCount = Number(firstPage.meta?.total_count) || 0;
|
|
114
|
-
const totalPages = Math.min(Math.ceil(totalCount / perPage), maxPages);
|
|
115
|
-
const allData = [...(firstPage.data || [])];
|
|
116
|
-
if (totalPages <= 1) {
|
|
117
|
-
return {
|
|
118
|
-
data: allData,
|
|
119
|
-
totalCount,
|
|
120
|
-
pagesFetched: 1,
|
|
121
|
-
duration: Date.now() - startTime,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
// Process remaining pages in parallel batches
|
|
125
|
-
const remainingPages = Array.from({ length: totalPages - 1 }, (_, i) => i + 2);
|
|
126
|
-
const semaphore = new Semaphore(maxConcurrency);
|
|
127
|
-
const pagePromises = remainingPages.map(async (pageNum) => {
|
|
128
|
-
await semaphore.acquire();
|
|
129
|
-
try {
|
|
130
|
-
const page = await this.getPage(endpoint, pageNum, perPage, params);
|
|
131
|
-
return page.data || [];
|
|
132
|
-
}
|
|
133
|
-
finally {
|
|
134
|
-
semaphore.release();
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
const pageResults = await Promise.all(pagePromises);
|
|
138
|
-
for (const pageData of pageResults) {
|
|
139
|
-
allData.push(...pageData);
|
|
140
|
-
if (onProgress) {
|
|
141
|
-
onProgress(allData.length, totalCount);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
data: allData,
|
|
146
|
-
totalCount,
|
|
147
|
-
pagesFetched: totalPages,
|
|
148
|
-
duration: Date.now() - startTime,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
exports.PaginationHelper = PaginationHelper;
|
|
153
|
-
/**
|
|
154
|
-
* Semaphore for controlling concurrency
|
|
155
|
-
*/
|
|
156
|
-
class Semaphore {
|
|
157
|
-
constructor(permits) {
|
|
158
|
-
this.waiting = [];
|
|
159
|
-
this.permits = permits;
|
|
160
|
-
}
|
|
161
|
-
async acquire() {
|
|
162
|
-
if (this.permits > 0) {
|
|
163
|
-
this.permits--;
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
return new Promise(resolve => {
|
|
167
|
-
this.waiting.push(resolve);
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
release() {
|
|
171
|
-
this.permits++;
|
|
172
|
-
if (this.waiting.length > 0) {
|
|
173
|
-
const resolve = this.waiting.shift();
|
|
174
|
-
this.permits--;
|
|
175
|
-
resolve();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { PcoClientState } from '../core';
|
|
2
|
-
import type { ErrorContext } from '@rachelallyson/planning-center-base-ts';
|
|
3
|
-
import { AddressAttributes, AddressesList, AddressSingle, EmailAttributes, EmailSingle, EmailsList, PhoneNumberAttributes, PhoneNumberSingle, PhoneNumbersList, SocialProfileAttributes, SocialProfileSingle, SocialProfilesList } from '../types';
|
|
4
|
-
/**
|
|
5
|
-
* Get all emails for a person
|
|
6
|
-
*/
|
|
7
|
-
export declare function getPersonEmails(client: PcoClientState, personId: string, context?: Partial<ErrorContext>): Promise<EmailsList>;
|
|
8
|
-
/**
|
|
9
|
-
* Create an email for a person
|
|
10
|
-
*/
|
|
11
|
-
export declare function createPersonEmail(client: PcoClientState, personId: string, data: Partial<EmailAttributes>, context?: Partial<ErrorContext>): Promise<EmailSingle>;
|
|
12
|
-
/**
|
|
13
|
-
* Get all phone numbers for a person
|
|
14
|
-
*/
|
|
15
|
-
export declare function getPersonPhoneNumbers(client: PcoClientState, personId: string, context?: Partial<ErrorContext>): Promise<PhoneNumbersList>;
|
|
16
|
-
/**
|
|
17
|
-
* Create a phone number for a person
|
|
18
|
-
*/
|
|
19
|
-
export declare function createPersonPhoneNumber(client: PcoClientState, personId: string, data: Partial<PhoneNumberAttributes>, context?: Partial<ErrorContext>): Promise<PhoneNumberSingle>;
|
|
20
|
-
/**
|
|
21
|
-
* Get all addresses for a person
|
|
22
|
-
*/
|
|
23
|
-
export declare function getPersonAddresses(client: PcoClientState, personId: string, context?: Partial<ErrorContext>): Promise<AddressesList>;
|
|
24
|
-
/**
|
|
25
|
-
* Create an address for a person
|
|
26
|
-
*/
|
|
27
|
-
export declare function createPersonAddress(client: PcoClientState, personId: string, data: Partial<AddressAttributes>, context?: Partial<ErrorContext>): Promise<AddressSingle>;
|
|
28
|
-
/**
|
|
29
|
-
* Update an address for a person
|
|
30
|
-
*/
|
|
31
|
-
export declare function updatePersonAddress(client: PcoClientState, personId: string, addressId: string, data: Partial<AddressAttributes>, context?: Partial<ErrorContext>): Promise<AddressSingle>;
|
|
32
|
-
/**
|
|
33
|
-
* Get social profiles for a person
|
|
34
|
-
*/
|
|
35
|
-
export declare function getPersonSocialProfiles(client: PcoClientState, personId: string, context?: Partial<ErrorContext>): Promise<SocialProfilesList>;
|
|
36
|
-
/**
|
|
37
|
-
* Create a social profile for a person
|
|
38
|
-
*/
|
|
39
|
-
export declare function createPersonSocialProfile(client: PcoClientState, personId: string, data: Partial<SocialProfileAttributes>, context?: Partial<ErrorContext>): Promise<SocialProfileSingle>;
|
|
40
|
-
/**
|
|
41
|
-
* Delete a social profile
|
|
42
|
-
*/
|
|
43
|
-
export declare function deleteSocialProfile(client: PcoClientState, socialProfileId: string, context?: Partial<ErrorContext>): Promise<void>;
|