@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.
- package/CHANGELOG.md +248 -1
- package/README.md +28 -0
- package/dist/auth.d.ts +64 -0
- package/dist/auth.js +98 -0
- package/dist/batch.d.ts +47 -0
- package/dist/batch.js +376 -0
- package/dist/client-manager.d.ts +66 -0
- package/dist/client-manager.js +150 -0
- package/dist/client.d.ts +71 -0
- package/dist/client.js +123 -0
- package/dist/core/http.d.ts +47 -0
- package/dist/core/http.js +242 -0
- package/dist/core/pagination.d.ts +34 -0
- package/dist/core/pagination.js +164 -0
- package/dist/core.d.ts +5 -0
- package/dist/core.js +12 -0
- package/dist/helpers.d.ts +125 -100
- package/dist/helpers.js +315 -275
- package/dist/index.d.ts +17 -5
- package/dist/index.js +39 -5
- package/dist/matching/matcher.d.ts +41 -0
- package/dist/matching/matcher.js +161 -0
- package/dist/matching/scoring.d.ts +35 -0
- package/dist/matching/scoring.js +141 -0
- package/dist/matching/strategies.d.ts +35 -0
- package/dist/matching/strategies.js +79 -0
- package/dist/modules/base.d.ts +46 -0
- package/dist/modules/base.js +82 -0
- package/dist/modules/contacts.d.ts +103 -0
- package/dist/modules/contacts.js +130 -0
- package/dist/modules/fields.d.ts +157 -0
- package/dist/modules/fields.js +294 -0
- package/dist/modules/households.d.ts +42 -0
- package/dist/modules/households.js +74 -0
- package/dist/modules/lists.d.ts +62 -0
- package/dist/modules/lists.js +92 -0
- package/dist/modules/notes.d.ts +74 -0
- package/dist/modules/notes.js +125 -0
- package/dist/modules/people.d.ts +196 -0
- package/dist/modules/people.js +221 -0
- package/dist/modules/workflows.d.ts +131 -0
- package/dist/modules/workflows.js +221 -0
- package/dist/monitoring.d.ts +53 -0
- package/dist/monitoring.js +142 -0
- package/dist/people/contacts.d.ts +43 -0
- package/dist/people/contacts.js +122 -0
- package/dist/people/core.d.ts +28 -0
- package/dist/people/core.js +69 -0
- package/dist/people/fields.d.ts +62 -0
- package/dist/people/fields.js +293 -0
- package/dist/people/households.d.ts +15 -0
- package/dist/people/households.js +31 -0
- package/dist/people/index.d.ts +8 -0
- package/dist/people/index.js +25 -0
- package/dist/people/lists.d.ts +30 -0
- package/dist/people/lists.js +37 -0
- package/dist/people/notes.d.ts +30 -0
- package/dist/people/notes.js +37 -0
- package/dist/people/organization.d.ts +12 -0
- package/dist/people/organization.js +15 -0
- package/dist/people/workflows.d.ts +37 -0
- package/dist/people/workflows.js +75 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.js +24 -0
- package/dist/testing/recorder.d.ts +58 -0
- package/dist/testing/recorder.js +195 -0
- package/dist/testing/simple-builders.d.ts +33 -0
- package/dist/testing/simple-builders.js +124 -0
- package/dist/testing/simple-factories.d.ts +91 -0
- package/dist/testing/simple-factories.js +279 -0
- package/dist/testing/types.d.ts +160 -0
- package/dist/testing/types.js +5 -0
- package/dist/types/batch.d.ts +50 -0
- package/dist/types/batch.js +5 -0
- package/dist/types/client.d.ts +81 -0
- package/dist/types/client.js +5 -0
- package/dist/types/events.d.ts +85 -0
- package/dist/types/events.js +5 -0
- package/dist/types/people.d.ts +73 -79
- package/package.json +14 -3
- package/dist/people.d.ts +0 -205
- 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();
|