@rachelallyson/planning-center-people-ts 1.0.0 → 2.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +248 -1
  2. package/README.md +28 -0
  3. package/dist/auth.d.ts +64 -0
  4. package/dist/auth.js +98 -0
  5. package/dist/batch.d.ts +47 -0
  6. package/dist/batch.js +376 -0
  7. package/dist/client-manager.d.ts +66 -0
  8. package/dist/client-manager.js +150 -0
  9. package/dist/client.d.ts +71 -0
  10. package/dist/client.js +123 -0
  11. package/dist/core/http.d.ts +47 -0
  12. package/dist/core/http.js +242 -0
  13. package/dist/core/pagination.d.ts +34 -0
  14. package/dist/core/pagination.js +164 -0
  15. package/dist/core.d.ts +5 -0
  16. package/dist/core.js +12 -0
  17. package/dist/helpers.d.ts +125 -100
  18. package/dist/helpers.js +315 -275
  19. package/dist/index.d.ts +17 -5
  20. package/dist/index.js +39 -5
  21. package/dist/matching/matcher.d.ts +41 -0
  22. package/dist/matching/matcher.js +161 -0
  23. package/dist/matching/scoring.d.ts +35 -0
  24. package/dist/matching/scoring.js +141 -0
  25. package/dist/matching/strategies.d.ts +35 -0
  26. package/dist/matching/strategies.js +79 -0
  27. package/dist/modules/base.d.ts +46 -0
  28. package/dist/modules/base.js +82 -0
  29. package/dist/modules/contacts.d.ts +103 -0
  30. package/dist/modules/contacts.js +130 -0
  31. package/dist/modules/fields.d.ts +157 -0
  32. package/dist/modules/fields.js +294 -0
  33. package/dist/modules/households.d.ts +42 -0
  34. package/dist/modules/households.js +74 -0
  35. package/dist/modules/lists.d.ts +62 -0
  36. package/dist/modules/lists.js +92 -0
  37. package/dist/modules/notes.d.ts +74 -0
  38. package/dist/modules/notes.js +125 -0
  39. package/dist/modules/people.d.ts +196 -0
  40. package/dist/modules/people.js +221 -0
  41. package/dist/modules/workflows.d.ts +131 -0
  42. package/dist/modules/workflows.js +221 -0
  43. package/dist/monitoring.d.ts +53 -0
  44. package/dist/monitoring.js +142 -0
  45. package/dist/people/contacts.d.ts +43 -0
  46. package/dist/people/contacts.js +122 -0
  47. package/dist/people/core.d.ts +28 -0
  48. package/dist/people/core.js +69 -0
  49. package/dist/people/fields.d.ts +62 -0
  50. package/dist/people/fields.js +293 -0
  51. package/dist/people/households.d.ts +15 -0
  52. package/dist/people/households.js +31 -0
  53. package/dist/people/index.d.ts +8 -0
  54. package/dist/people/index.js +25 -0
  55. package/dist/people/lists.d.ts +30 -0
  56. package/dist/people/lists.js +37 -0
  57. package/dist/people/notes.d.ts +30 -0
  58. package/dist/people/notes.js +37 -0
  59. package/dist/people/organization.d.ts +12 -0
  60. package/dist/people/organization.js +15 -0
  61. package/dist/people/workflows.d.ts +37 -0
  62. package/dist/people/workflows.js +75 -0
  63. package/dist/testing/index.d.ts +9 -0
  64. package/dist/testing/index.js +24 -0
  65. package/dist/testing/recorder.d.ts +58 -0
  66. package/dist/testing/recorder.js +195 -0
  67. package/dist/testing/simple-builders.d.ts +33 -0
  68. package/dist/testing/simple-builders.js +124 -0
  69. package/dist/testing/simple-factories.d.ts +91 -0
  70. package/dist/testing/simple-factories.js +279 -0
  71. package/dist/testing/types.d.ts +160 -0
  72. package/dist/testing/types.js +5 -0
  73. package/dist/types/batch.d.ts +50 -0
  74. package/dist/types/batch.js +5 -0
  75. package/dist/types/client.d.ts +81 -0
  76. package/dist/types/client.js +5 -0
  77. package/dist/types/events.d.ts +85 -0
  78. package/dist/types/events.js +5 -0
  79. package/dist/types/people.d.ts +73 -79
  80. package/package.json +14 -3
  81. package/dist/people.d.ts +0 -205
  82. package/dist/people.js +0 -598
package/dist/client.js ADDED
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * v2.0.0 Main PcoClient Class
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PcoClient = void 0;
7
+ const monitoring_1 = require("./monitoring");
8
+ const http_1 = require("./core/http");
9
+ const pagination_1 = require("./core/pagination");
10
+ const people_1 = require("./modules/people");
11
+ const fields_1 = require("./modules/fields");
12
+ const workflows_1 = require("./modules/workflows");
13
+ const contacts_1 = require("./modules/contacts");
14
+ const households_1 = require("./modules/households");
15
+ const notes_1 = require("./modules/notes");
16
+ const lists_1 = require("./modules/lists");
17
+ const batch_1 = require("./batch");
18
+ class PcoClient {
19
+ constructor(config) {
20
+ this.config = config;
21
+ this.eventEmitter = new monitoring_1.PcoEventEmitter();
22
+ this.httpClient = new http_1.PcoHttpClient(config, this.eventEmitter);
23
+ this.paginationHelper = new pagination_1.PaginationHelper(this.httpClient);
24
+ // Initialize modules
25
+ this.people = new people_1.PeopleModule(this.httpClient, this.paginationHelper, this.eventEmitter);
26
+ this.fields = new fields_1.FieldsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
27
+ this.workflows = new workflows_1.WorkflowsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
28
+ this.contacts = new contacts_1.ContactsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
29
+ this.households = new households_1.HouseholdsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
30
+ this.notes = new notes_1.NotesModule(this.httpClient, this.paginationHelper, this.eventEmitter);
31
+ this.lists = new lists_1.ListsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
32
+ this.batch = new batch_1.BatchExecutor(this, this.eventEmitter);
33
+ // Set up event handlers from config
34
+ this.setupEventHandlers();
35
+ }
36
+ // EventEmitter implementation
37
+ on(eventType, handler) {
38
+ this.eventEmitter.on(eventType, handler);
39
+ }
40
+ off(eventType, handler) {
41
+ this.eventEmitter.off(eventType, handler);
42
+ }
43
+ emit(event) {
44
+ this.eventEmitter.emit(event);
45
+ }
46
+ /**
47
+ * Get the current configuration
48
+ */
49
+ getConfig() {
50
+ return { ...this.config };
51
+ }
52
+ /**
53
+ * Update the configuration
54
+ */
55
+ updateConfig(updates) {
56
+ this.config = { ...this.config, ...updates };
57
+ // Recreate HTTP client with new config
58
+ this.httpClient = new http_1.PcoHttpClient(this.config, this.eventEmitter);
59
+ this.paginationHelper = new pagination_1.PaginationHelper(this.httpClient);
60
+ // Update modules with new HTTP client
61
+ this.updateModules();
62
+ }
63
+ /**
64
+ * Get performance metrics
65
+ */
66
+ getPerformanceMetrics() {
67
+ return this.httpClient.getPerformanceMetrics();
68
+ }
69
+ /**
70
+ * Get rate limit information
71
+ */
72
+ getRateLimitInfo() {
73
+ return this.httpClient.getRateLimitInfo();
74
+ }
75
+ /**
76
+ * Clear all event listeners
77
+ */
78
+ removeAllListeners(eventType) {
79
+ this.eventEmitter.removeAllListeners(eventType);
80
+ }
81
+ /**
82
+ * Get the number of listeners for an event type
83
+ */
84
+ listenerCount(eventType) {
85
+ return this.eventEmitter.listenerCount(eventType);
86
+ }
87
+ /**
88
+ * Get all registered event types
89
+ */
90
+ eventTypes() {
91
+ return this.eventEmitter.eventTypes();
92
+ }
93
+ setupEventHandlers() {
94
+ // Set up config event handlers
95
+ if (this.config.events?.onError) {
96
+ this.on('error', this.config.events.onError);
97
+ }
98
+ if (this.config.events?.onAuthFailure) {
99
+ this.on('auth:failure', this.config.events.onAuthFailure);
100
+ }
101
+ if (this.config.events?.onRequestStart) {
102
+ this.on('request:start', this.config.events.onRequestStart);
103
+ }
104
+ if (this.config.events?.onRequestComplete) {
105
+ this.on('request:complete', this.config.events.onRequestComplete);
106
+ }
107
+ if (this.config.events?.onRateLimit) {
108
+ this.on('rate:limit', this.config.events.onRateLimit);
109
+ }
110
+ }
111
+ updateModules() {
112
+ // Recreate modules with new HTTP client
113
+ this.people = new people_1.PeopleModule(this.httpClient, this.paginationHelper, this.eventEmitter);
114
+ this.fields = new fields_1.FieldsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
115
+ this.workflows = new workflows_1.WorkflowsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
116
+ this.contacts = new contacts_1.ContactsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
117
+ this.households = new households_1.HouseholdsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
118
+ this.notes = new notes_1.NotesModule(this.httpClient, this.paginationHelper, this.eventEmitter);
119
+ this.lists = new lists_1.ListsModule(this.httpClient, this.paginationHelper, this.eventEmitter);
120
+ this.batch = new batch_1.BatchExecutor(this, this.eventEmitter);
121
+ }
122
+ }
123
+ exports.PcoClient = PcoClient;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * v2.0.0 HTTP Client
3
+ */
4
+ import type { PcoClientConfig } from '../types/client';
5
+ import { PcoEventEmitter } from '../monitoring';
6
+ export interface HttpRequestOptions {
7
+ method: string;
8
+ endpoint: string;
9
+ data?: any;
10
+ params?: Record<string, any>;
11
+ headers?: Record<string, string>;
12
+ timeout?: number;
13
+ }
14
+ export interface HttpResponse<T = any> {
15
+ data: T;
16
+ status: number;
17
+ headers: Record<string, string>;
18
+ requestId: string;
19
+ duration: number;
20
+ }
21
+ export declare class PcoHttpClient {
22
+ private config;
23
+ private eventEmitter;
24
+ private requestIdGenerator;
25
+ private performanceMetrics;
26
+ private rateLimitTracker;
27
+ private rateLimiter;
28
+ constructor(config: PcoClientConfig, eventEmitter: PcoEventEmitter);
29
+ request<T = any>(options: HttpRequestOptions): Promise<HttpResponse<T>>;
30
+ private makeRequest;
31
+ private addAuthentication;
32
+ private getResourceTypeFromEndpoint;
33
+ private extractHeaders;
34
+ private updateRateLimitTracking;
35
+ getPerformanceMetrics(): Record<string, {
36
+ count: number;
37
+ averageTime: number;
38
+ minTime: number;
39
+ maxTime: number;
40
+ errorRate: number;
41
+ }>;
42
+ getRateLimitInfo(): Record<string, {
43
+ limit: number;
44
+ remaining: number;
45
+ resetTime: number;
46
+ }>;
47
+ }
@@ -0,0 +1,242 @@
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 monitoring_1 = require("../monitoring");
8
+ const rate_limiter_1 = require("../rate-limiter");
9
+ const api_error_1 = require("../api-error");
10
+ const auth_1 = require("../auth");
11
+ class PcoHttpClient {
12
+ constructor(config, eventEmitter) {
13
+ this.config = config;
14
+ this.eventEmitter = eventEmitter;
15
+ this.requestIdGenerator = new monitoring_1.RequestIdGenerator();
16
+ this.performanceMetrics = new monitoring_1.PerformanceMetrics();
17
+ this.rateLimitTracker = new monitoring_1.RateLimitTracker();
18
+ // Initialize rate limiter
19
+ this.rateLimiter = new rate_limiter_1.PcoRateLimiter(100, 60000); // 100 requests per minute
20
+ }
21
+ async request(options) {
22
+ const requestId = this.requestIdGenerator.generate();
23
+ const startTime = Date.now();
24
+ // Emit request start event
25
+ this.eventEmitter.emit({
26
+ type: 'request:start',
27
+ endpoint: options.endpoint,
28
+ method: options.method,
29
+ requestId,
30
+ timestamp: new Date().toISOString(),
31
+ });
32
+ try {
33
+ // Wait for rate limiter
34
+ await this.rateLimiter.waitForAvailability();
35
+ const response = await this.makeRequest(options, requestId);
36
+ const duration = Date.now() - startTime;
37
+ // Record performance metrics
38
+ this.performanceMetrics.record(`${options.method} ${options.endpoint}`, duration, true);
39
+ // Update rate limit tracking
40
+ this.updateRateLimitTracking(options.endpoint, response.headers);
41
+ // Emit request complete event
42
+ this.eventEmitter.emit({
43
+ type: 'request:complete',
44
+ endpoint: options.endpoint,
45
+ method: options.method,
46
+ status: response.status,
47
+ duration,
48
+ requestId,
49
+ timestamp: new Date().toISOString(),
50
+ });
51
+ return response;
52
+ }
53
+ catch (error) {
54
+ const duration = Date.now() - startTime;
55
+ // Record performance metrics
56
+ this.performanceMetrics.record(`${options.method} ${options.endpoint}`, duration, false);
57
+ // Emit request error event
58
+ this.eventEmitter.emit({
59
+ type: 'request:error',
60
+ endpoint: options.endpoint,
61
+ method: options.method,
62
+ error: error,
63
+ requestId,
64
+ timestamp: new Date().toISOString(),
65
+ });
66
+ throw error;
67
+ }
68
+ }
69
+ async makeRequest(options, requestId) {
70
+ const baseURL = this.config.baseURL || 'https://api.planningcenteronline.com/people/v2';
71
+ let url = options.endpoint.startsWith('http') ? options.endpoint : `${baseURL}${options.endpoint}`;
72
+ // Add query parameters
73
+ if (options.params) {
74
+ const searchParams = new URLSearchParams();
75
+ Object.entries(options.params).forEach(([key, value]) => {
76
+ if (value !== undefined && value !== null) {
77
+ searchParams.append(key, String(value));
78
+ }
79
+ });
80
+ const queryString = searchParams.toString();
81
+ if (queryString) {
82
+ url += url.includes('?') ? `&${queryString}` : `?${queryString}`;
83
+ }
84
+ }
85
+ // Prepare headers
86
+ const headers = {
87
+ 'Accept': 'application/json',
88
+ 'Content-Type': 'application/json',
89
+ ...this.config.headers,
90
+ ...options.headers,
91
+ };
92
+ // Add authentication
93
+ this.addAuthentication(headers);
94
+ // Prepare request options
95
+ const requestOptions = {
96
+ headers,
97
+ method: options.method,
98
+ };
99
+ // Add body for POST/PATCH requests
100
+ if ((options.method === 'POST' || options.method === 'PATCH') && options.data) {
101
+ // Determine resource type from endpoint
102
+ const resourceType = this.getResourceTypeFromEndpoint(options.endpoint);
103
+ // Separate attributes and relationships
104
+ const { relationships, ...attributes } = options.data;
105
+ const jsonApiData = {
106
+ data: {
107
+ type: resourceType,
108
+ attributes
109
+ }
110
+ };
111
+ // Add relationships if present
112
+ if (relationships) {
113
+ jsonApiData.data.relationships = relationships;
114
+ }
115
+ requestOptions.body = JSON.stringify(jsonApiData);
116
+ }
117
+ // Add timeout
118
+ const timeout = options.timeout || this.config.timeout || 30000;
119
+ const controller = new AbortController();
120
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
121
+ requestOptions.signal = controller.signal;
122
+ try {
123
+ const response = await fetch(url, requestOptions);
124
+ clearTimeout(timeoutId);
125
+ // Update rate limiter from headers
126
+ const rateLimitHeaders = {
127
+ 'Retry-After': response.headers.get('retry-after') || undefined,
128
+ 'X-PCO-API-Request-Rate-Count': response.headers.get('x-pco-api-request-rate-count') || undefined,
129
+ 'X-PCO-API-Request-Rate-Limit': response.headers.get('x-pco-api-request-rate-limit') || undefined,
130
+ 'X-PCO-API-Request-Rate-Period': response.headers.get('x-pco-api-request-rate-period') || undefined,
131
+ };
132
+ this.rateLimiter.updateFromHeaders(rateLimitHeaders);
133
+ this.rateLimiter.recordRequest();
134
+ // Handle 429 responses
135
+ if (response.status === 429) {
136
+ await this.rateLimiter.waitForAvailability();
137
+ return this.makeRequest(options, requestId);
138
+ }
139
+ // Handle other errors
140
+ if (!response.ok) {
141
+ // Handle 401 errors with token refresh if available
142
+ // Convert v2.0 config to v1.x format for auth functions
143
+ const v1Config = {
144
+ refreshToken: this.config.auth.refreshToken,
145
+ onTokenRefresh: this.config.auth.onRefresh,
146
+ onTokenRefreshFailure: this.config.auth.onRefreshFailure,
147
+ };
148
+ const clientState = { config: v1Config, rateLimiter: this.rateLimiter };
149
+ if (response.status === 401 && (0, auth_1.hasRefreshTokenCapability)(clientState)) {
150
+ try {
151
+ await (0, auth_1.attemptTokenRefresh)(clientState, () => this.makeRequest(options, requestId));
152
+ return this.makeRequest(options, requestId);
153
+ }
154
+ catch (refreshError) {
155
+ console.warn('Token refresh failed:', refreshError);
156
+ }
157
+ }
158
+ let errorData;
159
+ try {
160
+ errorData = await response.json();
161
+ }
162
+ catch {
163
+ errorData = {};
164
+ }
165
+ throw api_error_1.PcoApiError.fromFetchError(response, errorData);
166
+ }
167
+ // Parse response
168
+ if (options.method === 'DELETE') {
169
+ return {
170
+ data: undefined,
171
+ status: response.status,
172
+ headers: this.extractHeaders(response),
173
+ requestId,
174
+ duration: 0, // Will be set by caller
175
+ };
176
+ }
177
+ const data = await response.json();
178
+ return {
179
+ data,
180
+ status: response.status,
181
+ headers: this.extractHeaders(response),
182
+ requestId,
183
+ duration: 0, // Will be set by caller
184
+ };
185
+ }
186
+ catch (error) {
187
+ clearTimeout(timeoutId);
188
+ throw error;
189
+ }
190
+ }
191
+ addAuthentication(headers) {
192
+ if (this.config.auth.personalAccessToken) {
193
+ // Personal Access Tokens use HTTP Basic Auth format: app_id:secret
194
+ // The personalAccessToken should be in the format "app_id:secret"
195
+ headers.Authorization = `Basic ${Buffer.from(this.config.auth.personalAccessToken).toString('base64')}`;
196
+ }
197
+ else if (this.config.auth.accessToken) {
198
+ headers.Authorization = `Bearer ${this.config.auth.accessToken}`;
199
+ }
200
+ }
201
+ getResourceTypeFromEndpoint(endpoint) {
202
+ // Extract resource type from endpoint
203
+ // /households -> Household
204
+ // /people -> Person
205
+ // /emails -> Email
206
+ // etc.
207
+ const pathParts = endpoint.split('/').filter(part => part.length > 0);
208
+ const resourcePath = pathParts[pathParts.length - 1];
209
+ // Convert kebab-case to PascalCase and make singular
210
+ const pascalCase = resourcePath
211
+ .split('-')
212
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
213
+ .join('');
214
+ // Make singular (remove trailing 's' if it exists and the word is longer than 3 characters)
215
+ if (pascalCase.endsWith('s') && pascalCase.length > 3) {
216
+ return pascalCase.slice(0, -1);
217
+ }
218
+ return pascalCase;
219
+ }
220
+ extractHeaders(response) {
221
+ const headers = {};
222
+ response.headers.forEach((value, key) => {
223
+ headers[key] = value;
224
+ });
225
+ return headers;
226
+ }
227
+ updateRateLimitTracking(endpoint, headers) {
228
+ const limit = headers['x-pco-api-request-rate-limit'];
229
+ const remaining = headers['x-pco-api-request-rate-count'];
230
+ const resetTime = headers['retry-after'];
231
+ if (limit && remaining && resetTime) {
232
+ this.rateLimitTracker.update(endpoint, parseInt(limit), parseInt(remaining), Date.now() + parseInt(resetTime) * 1000);
233
+ }
234
+ }
235
+ getPerformanceMetrics() {
236
+ return this.performanceMetrics.getMetrics();
237
+ }
238
+ getRateLimitInfo() {
239
+ return this.rateLimitTracker.getAllLimits();
240
+ }
241
+ }
242
+ exports.PcoHttpClient = PcoHttpClient;
@@ -0,0 +1,34 @@
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
+ }
@@ -0,0 +1,164 @@
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
+ hasMore = !!response.data.links?.next;
39
+ page++;
40
+ if (onProgress) {
41
+ onProgress(allData.length, totalCount || allData.length);
42
+ }
43
+ // Add delay between requests to respect rate limits
44
+ if (hasMore && delay > 0) {
45
+ await new Promise(resolve => setTimeout(resolve, delay));
46
+ }
47
+ }
48
+ return {
49
+ data: allData,
50
+ totalCount,
51
+ pagesFetched: page - 1,
52
+ duration: Date.now() - startTime,
53
+ };
54
+ }
55
+ async getPage(endpoint, page = 1, perPage = 100, params = {}) {
56
+ const response = await this.httpClient.request({
57
+ method: 'GET',
58
+ endpoint,
59
+ params: {
60
+ ...params,
61
+ page,
62
+ per_page: perPage,
63
+ },
64
+ });
65
+ return response.data;
66
+ }
67
+ async *streamPages(endpoint, params = {}, options = {}) {
68
+ const { maxPages = 1000, perPage = 100, delay = 50, } = options;
69
+ let page = 1;
70
+ let hasMore = true;
71
+ while (hasMore && page <= maxPages) {
72
+ const response = await this.httpClient.request({
73
+ method: 'GET',
74
+ endpoint,
75
+ params: {
76
+ ...params,
77
+ page,
78
+ per_page: perPage,
79
+ },
80
+ });
81
+ if (response.data.data && Array.isArray(response.data.data)) {
82
+ yield response.data.data;
83
+ }
84
+ hasMore = !!response.data.links?.next;
85
+ page++;
86
+ if (hasMore && delay > 0) {
87
+ await new Promise(resolve => setTimeout(resolve, delay));
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Get all pages with parallel processing for better performance
93
+ */
94
+ async getAllPagesParallel(endpoint, params = {}, options = {}) {
95
+ const { maxPages = 1000, perPage = 100, maxConcurrency = 3, onProgress, } = options;
96
+ const startTime = Date.now();
97
+ // First, get the first page to determine total count
98
+ const firstPage = await this.getPage(endpoint, 1, perPage, params);
99
+ const totalCount = Number(firstPage.meta?.total_count) || 0;
100
+ const totalPages = Math.min(Math.ceil(totalCount / perPage), maxPages);
101
+ const allData = [...(firstPage.data || [])];
102
+ if (totalPages <= 1) {
103
+ return {
104
+ data: allData,
105
+ totalCount,
106
+ pagesFetched: 1,
107
+ duration: Date.now() - startTime,
108
+ };
109
+ }
110
+ // Process remaining pages in parallel batches
111
+ const remainingPages = Array.from({ length: totalPages - 1 }, (_, i) => i + 2);
112
+ const semaphore = new Semaphore(maxConcurrency);
113
+ const pagePromises = remainingPages.map(async (pageNum) => {
114
+ await semaphore.acquire();
115
+ try {
116
+ const page = await this.getPage(endpoint, pageNum, perPage, params);
117
+ return page.data || [];
118
+ }
119
+ finally {
120
+ semaphore.release();
121
+ }
122
+ });
123
+ const pageResults = await Promise.all(pagePromises);
124
+ for (const pageData of pageResults) {
125
+ allData.push(...pageData);
126
+ if (onProgress) {
127
+ onProgress(allData.length, totalCount);
128
+ }
129
+ }
130
+ return {
131
+ data: allData,
132
+ totalCount,
133
+ pagesFetched: totalPages,
134
+ duration: Date.now() - startTime,
135
+ };
136
+ }
137
+ }
138
+ exports.PaginationHelper = PaginationHelper;
139
+ /**
140
+ * Semaphore for controlling concurrency
141
+ */
142
+ class Semaphore {
143
+ constructor(permits) {
144
+ this.waiting = [];
145
+ this.permits = permits;
146
+ }
147
+ async acquire() {
148
+ if (this.permits > 0) {
149
+ this.permits--;
150
+ return;
151
+ }
152
+ return new Promise(resolve => {
153
+ this.waiting.push(resolve);
154
+ });
155
+ }
156
+ release() {
157
+ this.permits++;
158
+ if (this.waiting.length > 0) {
159
+ const resolve = this.waiting.shift();
160
+ this.permits--;
161
+ resolve();
162
+ }
163
+ }
164
+ }
package/dist/core.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type ErrorContext, PcoError } from './error-handling';
2
2
  import { PcoRateLimiter } from './rate-limiter';
3
3
  import { Paginated, ResourceObject, Response as JsonApiResponse } from './types';
4
+ import { type TokenRefreshCallback, type TokenRefreshFailureCallback } from './auth';
4
5
  export interface PcoClientConfig {
5
6
  /** Personal Access Token (for single-user apps) */
6
7
  personalAccessToken?: string;
@@ -8,6 +9,10 @@ export interface PcoClientConfig {
8
9
  accessToken?: string;
9
10
  /** OAuth 2.0 Refresh Token (for multi-user apps) */
10
11
  refreshToken?: string;
12
+ /** Callback to handle token refresh */
13
+ onTokenRefresh?: TokenRefreshCallback;
14
+ /** Callback to handle token refresh failures */
15
+ onTokenRefreshFailure?: TokenRefreshFailureCallback;
11
16
  /** App ID (for Personal Access Token auth) */
12
17
  appId?: string;
13
18
  /** App Secret (for Personal Access Token auth) */
package/dist/core.js CHANGED
@@ -11,6 +11,7 @@ exports.getAllPages = getAllPages;
11
11
  exports.getRateLimitInfo = getRateLimitInfo;
12
12
  const error_handling_1 = require("./error-handling");
13
13
  const rate_limiter_1 = require("./rate-limiter");
14
+ const auth_1 = require("./auth");
14
15
  // Re-export PcoApiError for convenience
15
16
  var api_error_1 = require("./api-error");
16
17
  Object.defineProperty(exports, "PcoApiError", { enumerable: true, get: function () { return api_error_1.PcoApiError; } });
@@ -211,6 +212,17 @@ async function makeFetchRequest(client, method, endpoint, data, params, context)
211
212
  }
212
213
  // Handle other errors
213
214
  if (!response.ok) {
215
+ // Handle 401 errors with token refresh if available
216
+ if (response.status === 401 && (0, auth_1.hasRefreshTokenCapability)(client)) {
217
+ try {
218
+ // Attempt to refresh the token and retry the request
219
+ return await (0, auth_1.attemptTokenRefresh)(client, () => makeFetchRequest(client, method, endpoint, data, params, context));
220
+ }
221
+ catch (refreshError) {
222
+ // If token refresh fails, fall through to normal error handling
223
+ console.warn('Token refresh failed:', refreshError);
224
+ }
225
+ }
214
226
  let errorData;
215
227
  try {
216
228
  errorData = await response.json();