@rachelallyson/planning-center-people-ts 1.1.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 +116 -0
- package/README.md +16 -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/index.d.ts +13 -3
- package/dist/index.js +23 -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/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 +20 -1
- package/package.json +9 -3
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v2.0.0 Client Manager with Caching
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PcoClientManager = void 0;
|
|
7
|
+
const client_1 = require("./client");
|
|
8
|
+
class PcoClientManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.clientCache = new Map();
|
|
11
|
+
this.configCache = new Map();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get the singleton instance
|
|
15
|
+
*/
|
|
16
|
+
static getInstance() {
|
|
17
|
+
if (!PcoClientManager.instance) {
|
|
18
|
+
PcoClientManager.instance = new PcoClientManager();
|
|
19
|
+
}
|
|
20
|
+
return PcoClientManager.instance;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get a client instance with the given configuration
|
|
24
|
+
*/
|
|
25
|
+
static getClient(config) {
|
|
26
|
+
return PcoClientManager.getInstance().getClient(config);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get a client instance for a specific church with config resolution
|
|
30
|
+
*/
|
|
31
|
+
static async getClientForChurch(churchId, configResolver) {
|
|
32
|
+
return PcoClientManager.getInstance().getClientForChurch(churchId, configResolver);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Clear the client cache
|
|
36
|
+
*/
|
|
37
|
+
static clearCache() {
|
|
38
|
+
PcoClientManager.getInstance().clearCache();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get a client instance with caching
|
|
42
|
+
*/
|
|
43
|
+
getClient(config) {
|
|
44
|
+
const configKey = this.generateConfigKey(config);
|
|
45
|
+
// Check if we have a cached client
|
|
46
|
+
let client = this.clientCache.get(configKey);
|
|
47
|
+
if (!client) {
|
|
48
|
+
// Create new client
|
|
49
|
+
client = new client_1.PcoClient(config);
|
|
50
|
+
this.clientCache.set(configKey, client);
|
|
51
|
+
this.configCache.set(configKey, { ...config });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Check if config has changed
|
|
55
|
+
const cachedConfig = this.configCache.get(configKey);
|
|
56
|
+
if (cachedConfig && this.hasConfigChanged(cachedConfig, config)) {
|
|
57
|
+
// Update client with new config
|
|
58
|
+
client.updateConfig(config);
|
|
59
|
+
this.configCache.set(configKey, { ...config });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return client;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get a client instance for a specific church
|
|
66
|
+
*/
|
|
67
|
+
async getClientForChurch(churchId, configResolver) {
|
|
68
|
+
const configKey = `church:${churchId}`;
|
|
69
|
+
// Check if we have a cached client
|
|
70
|
+
let client = this.clientCache.get(configKey);
|
|
71
|
+
if (!client) {
|
|
72
|
+
// Resolve configuration
|
|
73
|
+
const config = await configResolver(churchId);
|
|
74
|
+
// Create new client
|
|
75
|
+
client = new client_1.PcoClient(config);
|
|
76
|
+
this.clientCache.set(configKey, client);
|
|
77
|
+
this.configCache.set(configKey, { ...config });
|
|
78
|
+
}
|
|
79
|
+
return client;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clear the client cache
|
|
83
|
+
*/
|
|
84
|
+
clearCache() {
|
|
85
|
+
this.clientCache.clear();
|
|
86
|
+
this.configCache.clear();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Remove a specific client from cache
|
|
90
|
+
*/
|
|
91
|
+
removeClient(config) {
|
|
92
|
+
const configKey = this.generateConfigKey(config);
|
|
93
|
+
this.clientCache.delete(configKey);
|
|
94
|
+
this.configCache.delete(configKey);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Remove a church client from cache
|
|
98
|
+
*/
|
|
99
|
+
removeChurchClient(churchId) {
|
|
100
|
+
const configKey = `church:${churchId}`;
|
|
101
|
+
this.clientCache.delete(configKey);
|
|
102
|
+
this.configCache.delete(configKey);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get cache statistics
|
|
106
|
+
*/
|
|
107
|
+
getCacheStats() {
|
|
108
|
+
const churchClients = Array.from(this.clientCache.keys()).filter(key => key.startsWith('church:')).length;
|
|
109
|
+
return {
|
|
110
|
+
clientCount: this.clientCache.size,
|
|
111
|
+
configCount: this.configCache.size,
|
|
112
|
+
churchClients,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Generate a cache key for a configuration
|
|
117
|
+
*/
|
|
118
|
+
generateConfigKey(config) {
|
|
119
|
+
// Create a hash of the configuration
|
|
120
|
+
const configStr = JSON.stringify({
|
|
121
|
+
authType: config.auth.type,
|
|
122
|
+
hasAccessToken: !!config.auth.accessToken,
|
|
123
|
+
hasRefreshToken: !!config.auth.refreshToken,
|
|
124
|
+
hasPersonalAccessToken: !!config.auth.personalAccessToken,
|
|
125
|
+
baseURL: config.baseURL,
|
|
126
|
+
timeout: config.timeout,
|
|
127
|
+
});
|
|
128
|
+
// Simple hash function
|
|
129
|
+
let hash = 0;
|
|
130
|
+
for (let i = 0; i < configStr.length; i++) {
|
|
131
|
+
const char = configStr.charCodeAt(i);
|
|
132
|
+
hash = ((hash << 5) - hash) + char;
|
|
133
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
134
|
+
}
|
|
135
|
+
return `config:${Math.abs(hash)}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check if configuration has changed
|
|
139
|
+
*/
|
|
140
|
+
hasConfigChanged(oldConfig, newConfig) {
|
|
141
|
+
// Compare key configuration properties
|
|
142
|
+
return (oldConfig.auth.type !== newConfig.auth.type ||
|
|
143
|
+
oldConfig.auth.accessToken !== newConfig.auth.accessToken ||
|
|
144
|
+
oldConfig.auth.refreshToken !== newConfig.auth.refreshToken ||
|
|
145
|
+
oldConfig.auth.personalAccessToken !== newConfig.auth.personalAccessToken ||
|
|
146
|
+
oldConfig.baseURL !== newConfig.baseURL ||
|
|
147
|
+
oldConfig.timeout !== newConfig.timeout);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
exports.PcoClientManager = PcoClientManager;
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2.0.0 Main PcoClient Class
|
|
3
|
+
*/
|
|
4
|
+
import type { PcoClientConfig } from './types/client';
|
|
5
|
+
import type { EventEmitter } from './types/events';
|
|
6
|
+
import { PeopleModule } from './modules/people';
|
|
7
|
+
import { FieldsModule } from './modules/fields';
|
|
8
|
+
import { WorkflowsModule } from './modules/workflows';
|
|
9
|
+
import { ContactsModule } from './modules/contacts';
|
|
10
|
+
import { HouseholdsModule } from './modules/households';
|
|
11
|
+
import { NotesModule } from './modules/notes';
|
|
12
|
+
import { ListsModule } from './modules/lists';
|
|
13
|
+
import { BatchExecutor } from './batch';
|
|
14
|
+
export declare class PcoClient implements EventEmitter {
|
|
15
|
+
people: PeopleModule;
|
|
16
|
+
fields: FieldsModule;
|
|
17
|
+
workflows: WorkflowsModule;
|
|
18
|
+
contacts: ContactsModule;
|
|
19
|
+
households: HouseholdsModule;
|
|
20
|
+
notes: NotesModule;
|
|
21
|
+
lists: ListsModule;
|
|
22
|
+
batch: BatchExecutor;
|
|
23
|
+
private httpClient;
|
|
24
|
+
private paginationHelper;
|
|
25
|
+
private eventEmitter;
|
|
26
|
+
private config;
|
|
27
|
+
constructor(config: PcoClientConfig);
|
|
28
|
+
on<T extends import('./types/events').PcoEvent>(eventType: T['type'], handler: import('./types/events').EventHandler<T>): void;
|
|
29
|
+
off<T extends import('./types/events').PcoEvent>(eventType: T['type'], handler: import('./types/events').EventHandler<T>): void;
|
|
30
|
+
emit<T extends import('./types/events').PcoEvent>(event: T): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get the current configuration
|
|
33
|
+
*/
|
|
34
|
+
getConfig(): PcoClientConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Update the configuration
|
|
37
|
+
*/
|
|
38
|
+
updateConfig(updates: Partial<PcoClientConfig>): void;
|
|
39
|
+
/**
|
|
40
|
+
* Get performance metrics
|
|
41
|
+
*/
|
|
42
|
+
getPerformanceMetrics(): Record<string, {
|
|
43
|
+
count: number;
|
|
44
|
+
averageTime: number;
|
|
45
|
+
minTime: number;
|
|
46
|
+
maxTime: number;
|
|
47
|
+
errorRate: number;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Get rate limit information
|
|
51
|
+
*/
|
|
52
|
+
getRateLimitInfo(): Record<string, {
|
|
53
|
+
limit: number;
|
|
54
|
+
remaining: number;
|
|
55
|
+
resetTime: number;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Clear all event listeners
|
|
59
|
+
*/
|
|
60
|
+
removeAllListeners(eventType?: import('./types/events').EventType): void;
|
|
61
|
+
/**
|
|
62
|
+
* Get the number of listeners for an event type
|
|
63
|
+
*/
|
|
64
|
+
listenerCount(eventType: import('./types/events').EventType): number;
|
|
65
|
+
/**
|
|
66
|
+
* Get all registered event types
|
|
67
|
+
*/
|
|
68
|
+
eventTypes(): import('./types/events').EventType[];
|
|
69
|
+
private setupEventHandlers;
|
|
70
|
+
private updateModules;
|
|
71
|
+
}
|
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
|
+
}
|