@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,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/index.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
export { PcoClient } from './client';
|
|
2
|
+
export { PcoClientManager } from './client-manager';
|
|
3
|
+
export type { PcoClientConfig } from './types/client';
|
|
4
|
+
export type { PcoEvent, EventHandler, EventType } from './types/events';
|
|
5
|
+
export type { BatchOperation, BatchResult, BatchOptions, BatchSummary } from './types/batch';
|
|
6
|
+
export type { Paginated, Relationship, ResourceIdentifier, ResourceObject, } from './types';
|
|
7
|
+
export type { PersonResource, PersonAttributes, PersonSingle, PeopleList, EmailResource, EmailAttributes, PhoneNumberResource, PhoneNumberAttributes, AddressResource, AddressAttributes, SocialProfileResource, SocialProfileAttributes, } from './types';
|
|
8
|
+
export type { FieldDefinitionResource, FieldDefinitionAttributes, FieldDatumResource, FieldDatumAttributes, FieldOptionResource, FieldOptionAttributes, TabResource, TabAttributes, } from './types';
|
|
9
|
+
export type { WorkflowResource, WorkflowAttributes, WorkflowCardResource, WorkflowCardAttributes, WorkflowCardNoteResource, WorkflowCardNoteAttributes, } from './types';
|
|
10
|
+
export type { HouseholdResource, HouseholdAttributes, NoteResource, NoteAttributes, ListResource, ListAttributes, OrganizationResource, OrganizationAttributes, } from './types';
|
|
1
11
|
export * from './types';
|
|
2
|
-
export type { PcoClientConfig, PcoClientState } from './core';
|
|
12
|
+
export type { PcoClientConfig as PcoClientConfigV1, PcoClientState } from './core';
|
|
3
13
|
export { createPcoClient, del, getAllPages, getList, getRateLimitInfo, getSingle, patch, post, } from './core';
|
|
4
14
|
export type { TokenResponse, TokenRefreshCallback, TokenRefreshFailureCallback } from './auth';
|
|
5
15
|
export { attemptTokenRefresh, hasRefreshTokenCapability, refreshAccessToken, updateClientTokens, } from './auth';
|
|
@@ -9,8 +19,8 @@ export { PcoRateLimiter } from './rate-limiter';
|
|
|
9
19
|
export type { ErrorContext } from './error-handling';
|
|
10
20
|
export { ErrorCategory, ErrorSeverity, handleNetworkError, handleTimeoutError, handleValidationError, PcoError, retryWithBackoff, shouldNotRetry, withErrorBoundary, } from './error-handling';
|
|
11
21
|
export { createFieldDefinition, createFieldOption, createPerson, createPersonAddress, createPersonEmail, createPersonFieldData, createPersonPhoneNumber, createPersonSocialProfile, createWorkflowCard, createWorkflowCardNote, deleteFieldDefinition, deletePerson, deletePersonFieldData, deleteSocialProfile, getFieldDefinitions, getFieldOptions, getHousehold, getHouseholds, getTabs, getListById, getListCategories, getLists, getNote, getNoteCategories, getNotes, getOrganization, getPeople, getPerson, getPersonAddresses, getPersonEmails, getPersonFieldData, getPersonPhoneNumbers, getPersonSocialProfiles, getWorkflow, getWorkflowCardNotes, getWorkflowCards, getWorkflows, updatePerson, updatePersonAddress, } from './people';
|
|
12
|
-
export type { Paginated, Relationship, ResourceIdentifier, ResourceObject, } from './types';
|
|
13
|
-
export type { AddressAttributes, AddressesList, AddressResource, AddressSingle, EmailAttributes, EmailResource, EmailSingle, EmailsList, FieldDataList, FieldDataSingle, FieldDatumAttributes, FieldDatumRelationships, FieldDatumResource, FieldDefinitionAttributes, FieldDefinitionResource, FieldDefinitionSingle, FieldDefinitionsList, FieldOptionAttributes, FieldOptionResource, FieldOptionSingle, FieldOptionsList, HouseholdAttributes, HouseholdResource, HouseholdSingle, HouseholdsList, ListAttributes, ListCategoriesList, ListCategoryAttributes, ListCategoryResource, ListCategorySingle, ListResource, ListSingle, ListsList, NoteAttributes, NoteCategoriesList, NoteCategoryAttributes, NoteCategoryResource, NoteCategorySingle, NoteResource, NoteSingle, NotesList, OrganizationAttributes, OrganizationResource, OrganizationSingle, PeopleList, PersonAttributes, PersonRelationships, PersonResource, PersonSingle, PhoneNumberAttributes, PhoneNumberResource, PhoneNumberSingle, PhoneNumbersList, SocialProfileAttributes, SocialProfileResource, SocialProfileSingle, SocialProfilesList, WorkflowAttributes, WorkflowCardAttributes, WorkflowCardNoteAttributes, WorkflowCardNoteResource, WorkflowCardNoteSingle, WorkflowCardNotesList, WorkflowCardRelationships, WorkflowCardResource, WorkflowCardSingle, WorkflowCardsList, WorkflowResource, WorkflowSingle, WorkflowsList, } from './types';
|
|
14
22
|
export { attemptRecovery, CircuitBreaker, classifyError, createErrorReport, DEFAULT_RETRY_CONFIG, executeBulkOperation, retryWithExponentialBackoff, TIMEOUT_CONFIG, withTimeout, } from './error-scenarios';
|
|
15
23
|
export { buildQueryParams, calculateAge, createPersonWithContact, createWorkflowCardWithNote, exportAllPeopleData, extractFileUrl, formatDate, formatPersonName, getCompletePersonProfile, getFileExtension, getFilename, getListsWithCategories, getOrganizationInfo, getPeopleByHousehold, getPersonWorkflowCardsWithNotes, getPrimaryContact, isFileUpload, isFileUrl, isValidEmail, isValidPhone, processFileValue, searchPeople, validatePersonData, } from './helpers';
|
|
16
24
|
export { AdaptiveRateLimiter, ApiCache, batchFetchPersonDetails, fetchAllPages, getCachedPeople, monitorPerformance, PerformanceMonitor, processInBatches, processLargeDataset, streamPeopleData, } from './performance';
|
|
25
|
+
export { MockPcoClient, MockResponseBuilder, RequestRecorder, createMockClient, createRecordingClient, createTestClient, createErrorMockClient, createSlowMockClient, } from './testing';
|
|
26
|
+
export type { MockClientConfig, RecordingConfig } from './testing';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// ===== v2.0.0 Main Exports =====
|
|
2
3
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
4
|
if (k2 === undefined) k2 = k;
|
|
4
5
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -14,10 +15,17 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
15
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
16
|
};
|
|
16
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.
|
|
18
|
-
exports.
|
|
19
|
-
exports.streamPeopleData = exports.processLargeDataset = exports.processInBatches = exports.PerformanceMonitor = void 0;
|
|
20
|
-
//
|
|
18
|
+
exports.getNotes = exports.getNoteCategories = exports.getNote = exports.getLists = exports.getListCategories = exports.getListById = exports.getTabs = exports.getHouseholds = exports.getHousehold = exports.getFieldOptions = exports.getFieldDefinitions = exports.deleteSocialProfile = exports.deletePersonFieldData = exports.deletePerson = exports.deleteFieldDefinition = exports.createWorkflowCardNote = exports.createWorkflowCard = exports.createPersonSocialProfile = exports.createPersonPhoneNumber = exports.createPersonFieldData = exports.createPersonEmail = exports.createPersonAddress = exports.createPerson = exports.createFieldOption = exports.createFieldDefinition = exports.withErrorBoundary = exports.shouldNotRetry = exports.retryWithBackoff = exports.PcoError = exports.handleValidationError = exports.handleTimeoutError = exports.handleNetworkError = exports.ErrorSeverity = exports.ErrorCategory = exports.PcoRateLimiter = exports.PcoApiError = exports.updateClientTokens = exports.refreshAccessToken = exports.hasRefreshTokenCapability = exports.attemptTokenRefresh = exports.post = exports.patch = exports.getSingle = exports.getRateLimitInfo = exports.getList = exports.getAllPages = exports.del = exports.createPcoClient = exports.PcoClientManager = exports.PcoClient = void 0;
|
|
19
|
+
exports.fetchAllPages = exports.batchFetchPersonDetails = exports.ApiCache = exports.AdaptiveRateLimiter = exports.validatePersonData = exports.searchPeople = exports.processFileValue = exports.isValidPhone = exports.isValidEmail = exports.isFileUrl = exports.isFileUpload = exports.getPrimaryContact = exports.getPersonWorkflowCardsWithNotes = exports.getPeopleByHousehold = exports.getOrganizationInfo = exports.getListsWithCategories = exports.getFilename = exports.getFileExtension = exports.getCompletePersonProfile = exports.formatPersonName = exports.formatDate = exports.extractFileUrl = exports.exportAllPeopleData = exports.createWorkflowCardWithNote = exports.createPersonWithContact = exports.calculateAge = exports.buildQueryParams = exports.withTimeout = exports.TIMEOUT_CONFIG = exports.retryWithExponentialBackoff = exports.executeBulkOperation = exports.DEFAULT_RETRY_CONFIG = exports.createErrorReport = exports.classifyError = exports.CircuitBreaker = exports.attemptRecovery = exports.updatePersonAddress = exports.updatePerson = exports.getWorkflows = exports.getWorkflowCards = exports.getWorkflowCardNotes = exports.getWorkflow = exports.getPersonSocialProfiles = exports.getPersonPhoneNumbers = exports.getPersonFieldData = exports.getPersonEmails = exports.getPersonAddresses = exports.getPerson = exports.getPeople = exports.getOrganization = void 0;
|
|
20
|
+
exports.createSlowMockClient = exports.createErrorMockClient = exports.createTestClient = exports.createRecordingClient = exports.createMockClient = exports.RequestRecorder = exports.MockResponseBuilder = exports.MockPcoClient = exports.streamPeopleData = exports.processLargeDataset = exports.processInBatches = exports.PerformanceMonitor = exports.monitorPerformance = exports.getCachedPeople = void 0;
|
|
21
|
+
// Main client class
|
|
22
|
+
var client_1 = require("./client");
|
|
23
|
+
Object.defineProperty(exports, "PcoClient", { enumerable: true, get: function () { return client_1.PcoClient; } });
|
|
24
|
+
// Client manager for caching and lifecycle management
|
|
25
|
+
var client_manager_1 = require("./client-manager");
|
|
26
|
+
Object.defineProperty(exports, "PcoClientManager", { enumerable: true, get: function () { return client_manager_1.PcoClientManager; } });
|
|
27
|
+
// ===== v1.x Compatibility Exports (Deprecated) =====
|
|
28
|
+
// Export all types for backward compatibility
|
|
21
29
|
__exportStar(require("./types"), exports);
|
|
22
30
|
var core_1 = require("./core");
|
|
23
31
|
Object.defineProperty(exports, "createPcoClient", { enumerable: true, get: function () { return core_1.createPcoClient; } });
|
|
@@ -48,7 +56,7 @@ Object.defineProperty(exports, "PcoError", { enumerable: true, get: function ()
|
|
|
48
56
|
Object.defineProperty(exports, "retryWithBackoff", { enumerable: true, get: function () { return error_handling_1.retryWithBackoff; } });
|
|
49
57
|
Object.defineProperty(exports, "shouldNotRetry", { enumerable: true, get: function () { return error_handling_1.shouldNotRetry; } });
|
|
50
58
|
Object.defineProperty(exports, "withErrorBoundary", { enumerable: true, get: function () { return error_handling_1.withErrorBoundary; } });
|
|
51
|
-
// Export People-specific functions
|
|
59
|
+
// Export People-specific functions (deprecated)
|
|
52
60
|
var people_1 = require("./people");
|
|
53
61
|
Object.defineProperty(exports, "createFieldDefinition", { enumerable: true, get: function () { return people_1.createFieldDefinition; } });
|
|
54
62
|
Object.defineProperty(exports, "createFieldOption", { enumerable: true, get: function () { return people_1.createFieldOption; } });
|
|
@@ -137,3 +145,13 @@ Object.defineProperty(exports, "PerformanceMonitor", { enumerable: true, get: fu
|
|
|
137
145
|
Object.defineProperty(exports, "processInBatches", { enumerable: true, get: function () { return performance_1.processInBatches; } });
|
|
138
146
|
Object.defineProperty(exports, "processLargeDataset", { enumerable: true, get: function () { return performance_1.processLargeDataset; } });
|
|
139
147
|
Object.defineProperty(exports, "streamPeopleData", { enumerable: true, get: function () { return performance_1.streamPeopleData; } });
|
|
148
|
+
// ===== Testing Utilities =====
|
|
149
|
+
var testing_1 = require("./testing");
|
|
150
|
+
Object.defineProperty(exports, "MockPcoClient", { enumerable: true, get: function () { return testing_1.MockPcoClient; } });
|
|
151
|
+
Object.defineProperty(exports, "MockResponseBuilder", { enumerable: true, get: function () { return testing_1.MockResponseBuilder; } });
|
|
152
|
+
Object.defineProperty(exports, "RequestRecorder", { enumerable: true, get: function () { return testing_1.RequestRecorder; } });
|
|
153
|
+
Object.defineProperty(exports, "createMockClient", { enumerable: true, get: function () { return testing_1.createMockClient; } });
|
|
154
|
+
Object.defineProperty(exports, "createRecordingClient", { enumerable: true, get: function () { return testing_1.createRecordingClient; } });
|
|
155
|
+
Object.defineProperty(exports, "createTestClient", { enumerable: true, get: function () { return testing_1.createTestClient; } });
|
|
156
|
+
Object.defineProperty(exports, "createErrorMockClient", { enumerable: true, get: function () { return testing_1.createErrorMockClient; } });
|
|
157
|
+
Object.defineProperty(exports, "createSlowMockClient", { enumerable: true, get: function () { return testing_1.createSlowMockClient; } });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2.0.0 Person Matching Logic
|
|
3
|
+
*/
|
|
4
|
+
import type { PeopleModule } from '../modules/people';
|
|
5
|
+
import type { PersonResource } from '../types';
|
|
6
|
+
import { PersonMatchOptions } from '../modules/people';
|
|
7
|
+
export interface MatchResult {
|
|
8
|
+
person: PersonResource;
|
|
9
|
+
score: number;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class PersonMatcher {
|
|
13
|
+
private peopleModule;
|
|
14
|
+
private strategies;
|
|
15
|
+
private scorer;
|
|
16
|
+
constructor(peopleModule: PeopleModule);
|
|
17
|
+
/**
|
|
18
|
+
* Find or create a person with smart matching
|
|
19
|
+
*/
|
|
20
|
+
findOrCreate(options: PersonMatchOptions): Promise<PersonResource>;
|
|
21
|
+
/**
|
|
22
|
+
* Find the best match for a person
|
|
23
|
+
*/
|
|
24
|
+
findMatch(options: PersonMatchOptions): Promise<MatchResult | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Get potential matching candidates
|
|
27
|
+
*/
|
|
28
|
+
private getCandidates;
|
|
29
|
+
/**
|
|
30
|
+
* Create a new person
|
|
31
|
+
*/
|
|
32
|
+
private createPerson;
|
|
33
|
+
/**
|
|
34
|
+
* Get all potential matches with detailed scoring
|
|
35
|
+
*/
|
|
36
|
+
getAllMatches(options: PersonMatchOptions): Promise<MatchResult[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Check if a person matches the given criteria
|
|
39
|
+
*/
|
|
40
|
+
isMatch(personId: string, options: PersonMatchOptions): Promise<MatchResult | null>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v2.0.0 Person Matching Logic
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PersonMatcher = void 0;
|
|
7
|
+
const strategies_1 = require("./strategies");
|
|
8
|
+
const scoring_1 = require("./scoring");
|
|
9
|
+
class PersonMatcher {
|
|
10
|
+
constructor(peopleModule) {
|
|
11
|
+
this.peopleModule = peopleModule;
|
|
12
|
+
this.strategies = new strategies_1.MatchStrategies();
|
|
13
|
+
this.scorer = new scoring_1.MatchScorer();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Find or create a person with smart matching
|
|
17
|
+
*/
|
|
18
|
+
async findOrCreate(options) {
|
|
19
|
+
const { createIfNotFound = true, matchStrategy = 'fuzzy', ...searchOptions } = options;
|
|
20
|
+
// Try to find existing person
|
|
21
|
+
const match = await this.findMatch({ ...searchOptions, matchStrategy });
|
|
22
|
+
if (match) {
|
|
23
|
+
return match.person;
|
|
24
|
+
}
|
|
25
|
+
// Create new person if not found and creation is enabled
|
|
26
|
+
if (createIfNotFound) {
|
|
27
|
+
return this.createPerson(options);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`No matching person found and creation is disabled`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Find the best match for a person
|
|
33
|
+
*/
|
|
34
|
+
async findMatch(options) {
|
|
35
|
+
const { matchStrategy = 'fuzzy' } = options;
|
|
36
|
+
// Get all potential matches
|
|
37
|
+
const candidates = await this.getCandidates(options);
|
|
38
|
+
if (candidates.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// Score and rank candidates
|
|
42
|
+
const scoredCandidates = candidates.map(candidate => ({
|
|
43
|
+
person: candidate,
|
|
44
|
+
score: this.scorer.scoreMatch(candidate, options),
|
|
45
|
+
reason: this.scorer.getMatchReason(candidate, options),
|
|
46
|
+
}));
|
|
47
|
+
// Sort by score (highest first)
|
|
48
|
+
scoredCandidates.sort((a, b) => b.score - a.score);
|
|
49
|
+
// Apply strategy-specific filtering
|
|
50
|
+
const bestMatch = this.strategies.selectBestMatch(scoredCandidates, matchStrategy);
|
|
51
|
+
return bestMatch;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get potential matching candidates
|
|
55
|
+
*/
|
|
56
|
+
async getCandidates(options) {
|
|
57
|
+
const candidates = [];
|
|
58
|
+
const { email, phone, firstName, lastName } = options;
|
|
59
|
+
// Strategy 1: Exact email match
|
|
60
|
+
if (email) {
|
|
61
|
+
try {
|
|
62
|
+
const emailMatches = await this.peopleModule.search({ email });
|
|
63
|
+
candidates.push(...emailMatches.data);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// Email search failed, continue with other strategies
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Strategy 2: Exact phone match
|
|
70
|
+
if (phone) {
|
|
71
|
+
try {
|
|
72
|
+
const phoneMatches = await this.peopleModule.search({ phone });
|
|
73
|
+
candidates.push(...phoneMatches.data);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Phone search failed, continue with other strategies
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Strategy 3: Name-based search
|
|
80
|
+
if (firstName && lastName) {
|
|
81
|
+
try {
|
|
82
|
+
const nameMatches = await this.peopleModule.search({
|
|
83
|
+
name: `${firstName} ${lastName}`
|
|
84
|
+
});
|
|
85
|
+
candidates.push(...nameMatches.data);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
// Name search failed, continue with other strategies
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Strategy 4: Broader search if no exact matches
|
|
92
|
+
if (candidates.length === 0 && (firstName || lastName)) {
|
|
93
|
+
try {
|
|
94
|
+
const broadMatches = await this.peopleModule.search({
|
|
95
|
+
name: firstName || lastName || '',
|
|
96
|
+
});
|
|
97
|
+
candidates.push(...broadMatches.data);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
// Broad search failed
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Remove duplicates based on person ID
|
|
104
|
+
const uniqueCandidates = candidates.filter((person, index, self) => index === self.findIndex(p => p.id === person.id));
|
|
105
|
+
return uniqueCandidates;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Create a new person
|
|
109
|
+
*/
|
|
110
|
+
async createPerson(options) {
|
|
111
|
+
const personData = {};
|
|
112
|
+
if (options.firstName)
|
|
113
|
+
personData.first_name = options.firstName;
|
|
114
|
+
if (options.lastName)
|
|
115
|
+
personData.last_name = options.lastName;
|
|
116
|
+
if (options.email)
|
|
117
|
+
personData.email = options.email;
|
|
118
|
+
if (options.phone)
|
|
119
|
+
personData.phone = options.phone;
|
|
120
|
+
const person = await this.peopleModule.create(personData);
|
|
121
|
+
// Add contact information if provided
|
|
122
|
+
const contacts = {};
|
|
123
|
+
if (options.email) {
|
|
124
|
+
contacts.email = { address: options.email, primary: true };
|
|
125
|
+
}
|
|
126
|
+
if (options.phone) {
|
|
127
|
+
contacts.phone = { number: options.phone, primary: true };
|
|
128
|
+
}
|
|
129
|
+
if (Object.keys(contacts).length > 0) {
|
|
130
|
+
await this.peopleModule.createWithContacts(personData, contacts);
|
|
131
|
+
}
|
|
132
|
+
return person;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get all potential matches with detailed scoring
|
|
136
|
+
*/
|
|
137
|
+
async getAllMatches(options) {
|
|
138
|
+
const candidates = await this.getCandidates(options);
|
|
139
|
+
return candidates.map(candidate => ({
|
|
140
|
+
person: candidate,
|
|
141
|
+
score: this.scorer.scoreMatch(candidate, options),
|
|
142
|
+
reason: this.scorer.getMatchReason(candidate, options),
|
|
143
|
+
})).sort((a, b) => b.score - a.score);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if a person matches the given criteria
|
|
147
|
+
*/
|
|
148
|
+
async isMatch(personId, options) {
|
|
149
|
+
const person = await this.peopleModule.getById(personId);
|
|
150
|
+
const score = this.scorer.scoreMatch(person, options);
|
|
151
|
+
if (score > 0.5) { // Threshold for considering it a match
|
|
152
|
+
return {
|
|
153
|
+
person,
|
|
154
|
+
score,
|
|
155
|
+
reason: this.scorer.getMatchReason(person, options),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.PersonMatcher = PersonMatcher;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2.0.0 Person Match Scoring
|
|
3
|
+
*/
|
|
4
|
+
import type { PersonResource } from '../types';
|
|
5
|
+
import type { PersonMatchOptions } from '../modules/people';
|
|
6
|
+
export declare class MatchScorer {
|
|
7
|
+
/**
|
|
8
|
+
* Score a person match based on various criteria
|
|
9
|
+
*/
|
|
10
|
+
scoreMatch(person: PersonResource, options: PersonMatchOptions): number;
|
|
11
|
+
/**
|
|
12
|
+
* Get a human-readable reason for the match
|
|
13
|
+
*/
|
|
14
|
+
getMatchReason(person: PersonResource, options: PersonMatchOptions): string;
|
|
15
|
+
/**
|
|
16
|
+
* Score email matching
|
|
17
|
+
*/
|
|
18
|
+
private scoreEmailMatch;
|
|
19
|
+
/**
|
|
20
|
+
* Score phone matching
|
|
21
|
+
*/
|
|
22
|
+
private scorePhoneMatch;
|
|
23
|
+
/**
|
|
24
|
+
* Score name matching
|
|
25
|
+
*/
|
|
26
|
+
private scoreNameMatch;
|
|
27
|
+
/**
|
|
28
|
+
* Score additional criteria
|
|
29
|
+
*/
|
|
30
|
+
private scoreAdditionalCriteria;
|
|
31
|
+
/**
|
|
32
|
+
* Calculate string similarity using Levenshtein distance
|
|
33
|
+
*/
|
|
34
|
+
private calculateStringSimilarity;
|
|
35
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v2.0.0 Person Match Scoring
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MatchScorer = void 0;
|
|
7
|
+
class MatchScorer {
|
|
8
|
+
/**
|
|
9
|
+
* Score a person match based on various criteria
|
|
10
|
+
*/
|
|
11
|
+
scoreMatch(person, options) {
|
|
12
|
+
let totalScore = 0;
|
|
13
|
+
let maxScore = 0;
|
|
14
|
+
// Email matching (highest weight)
|
|
15
|
+
if (options.email) {
|
|
16
|
+
const emailScore = this.scoreEmailMatch(person, options.email);
|
|
17
|
+
totalScore += emailScore * 0.4;
|
|
18
|
+
maxScore += 0.4;
|
|
19
|
+
}
|
|
20
|
+
// Phone matching (high weight)
|
|
21
|
+
if (options.phone) {
|
|
22
|
+
const phoneScore = this.scorePhoneMatch(person, options.phone);
|
|
23
|
+
totalScore += phoneScore * 0.3;
|
|
24
|
+
maxScore += 0.3;
|
|
25
|
+
}
|
|
26
|
+
// Name matching (medium weight)
|
|
27
|
+
if (options.firstName || options.lastName) {
|
|
28
|
+
const nameScore = this.scoreNameMatch(person, options);
|
|
29
|
+
totalScore += nameScore * 0.2;
|
|
30
|
+
maxScore += 0.2;
|
|
31
|
+
}
|
|
32
|
+
// Additional criteria (lower weight)
|
|
33
|
+
const additionalScore = this.scoreAdditionalCriteria(person, options);
|
|
34
|
+
totalScore += additionalScore * 0.1;
|
|
35
|
+
maxScore += 0.1;
|
|
36
|
+
return maxScore > 0 ? totalScore / maxScore : 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get a human-readable reason for the match
|
|
40
|
+
*/
|
|
41
|
+
getMatchReason(person, options) {
|
|
42
|
+
const reasons = [];
|
|
43
|
+
if (options.email && this.scoreEmailMatch(person, options.email) > 0.8) {
|
|
44
|
+
reasons.push('exact email match');
|
|
45
|
+
}
|
|
46
|
+
if (options.phone && this.scorePhoneMatch(person, options.phone) > 0.8) {
|
|
47
|
+
reasons.push('exact phone match');
|
|
48
|
+
}
|
|
49
|
+
if (options.firstName || options.lastName) {
|
|
50
|
+
const nameScore = this.scoreNameMatch(person, options);
|
|
51
|
+
if (nameScore > 0.8) {
|
|
52
|
+
reasons.push('exact name match');
|
|
53
|
+
}
|
|
54
|
+
else if (nameScore > 0.6) {
|
|
55
|
+
reasons.push('similar name match');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (reasons.length === 0) {
|
|
59
|
+
return 'partial match';
|
|
60
|
+
}
|
|
61
|
+
return reasons.join(', ');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Score email matching
|
|
65
|
+
*/
|
|
66
|
+
scoreEmailMatch(person, email) {
|
|
67
|
+
// This would need to check the person's emails
|
|
68
|
+
// For now, return a placeholder score
|
|
69
|
+
// In a real implementation, you'd fetch the person's emails and compare
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Score phone matching
|
|
74
|
+
*/
|
|
75
|
+
scorePhoneMatch(person, phone) {
|
|
76
|
+
// This would need to check the person's phone numbers
|
|
77
|
+
// For now, return a placeholder score
|
|
78
|
+
// In a real implementation, you'd fetch the person's phone numbers and compare
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Score name matching
|
|
83
|
+
*/
|
|
84
|
+
scoreNameMatch(person, options) {
|
|
85
|
+
const attrs = person.attributes;
|
|
86
|
+
if (!attrs)
|
|
87
|
+
return 0;
|
|
88
|
+
let score = 0;
|
|
89
|
+
// First name matching
|
|
90
|
+
if (options.firstName && attrs.first_name) {
|
|
91
|
+
const firstNameScore = this.calculateStringSimilarity(options.firstName.toLowerCase(), attrs.first_name.toLowerCase());
|
|
92
|
+
score += firstNameScore * 0.5;
|
|
93
|
+
}
|
|
94
|
+
// Last name matching
|
|
95
|
+
if (options.lastName && attrs.last_name) {
|
|
96
|
+
const lastNameScore = this.calculateStringSimilarity(options.lastName.toLowerCase(), attrs.last_name.toLowerCase());
|
|
97
|
+
score += lastNameScore * 0.5;
|
|
98
|
+
}
|
|
99
|
+
return score;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Score additional criteria
|
|
103
|
+
*/
|
|
104
|
+
scoreAdditionalCriteria(person, options) {
|
|
105
|
+
// Add scoring for other criteria like campus, status, etc.
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Calculate string similarity using Levenshtein distance
|
|
110
|
+
*/
|
|
111
|
+
calculateStringSimilarity(str1, str2) {
|
|
112
|
+
if (str1 === str2)
|
|
113
|
+
return 1;
|
|
114
|
+
if (str1.length === 0 || str2.length === 0)
|
|
115
|
+
return 0;
|
|
116
|
+
const matrix = [];
|
|
117
|
+
const len1 = str1.length;
|
|
118
|
+
const len2 = str2.length;
|
|
119
|
+
// Initialize matrix
|
|
120
|
+
for (let i = 0; i <= len1; i++) {
|
|
121
|
+
matrix[i] = [i];
|
|
122
|
+
}
|
|
123
|
+
for (let j = 0; j <= len2; j++) {
|
|
124
|
+
matrix[0][j] = j;
|
|
125
|
+
}
|
|
126
|
+
// Fill matrix
|
|
127
|
+
for (let i = 1; i <= len1; i++) {
|
|
128
|
+
for (let j = 1; j <= len2; j++) {
|
|
129
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
130
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
|
|
131
|
+
matrix[i][j - 1] + 1, // insertion
|
|
132
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const distance = matrix[len1][len2];
|
|
137
|
+
const maxLength = Math.max(len1, len2);
|
|
138
|
+
return 1 - distance / maxLength;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.MatchScorer = MatchScorer;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2.0.0 Person Matching Strategies
|
|
3
|
+
*/
|
|
4
|
+
import type { MatchResult } from './matcher';
|
|
5
|
+
export type MatchStrategy = 'exact' | 'fuzzy' | 'aggressive';
|
|
6
|
+
export declare class MatchStrategies {
|
|
7
|
+
/**
|
|
8
|
+
* Select the best match based on strategy
|
|
9
|
+
*/
|
|
10
|
+
selectBestMatch(candidates: MatchResult[], strategy: MatchStrategy): MatchResult | null;
|
|
11
|
+
/**
|
|
12
|
+
* Exact matching strategy - only return matches with very high confidence
|
|
13
|
+
*/
|
|
14
|
+
private selectExactMatch;
|
|
15
|
+
/**
|
|
16
|
+
* Fuzzy matching strategy - return best match above threshold
|
|
17
|
+
*/
|
|
18
|
+
private selectFuzzyMatch;
|
|
19
|
+
/**
|
|
20
|
+
* Aggressive matching strategy - return best match with lower threshold
|
|
21
|
+
*/
|
|
22
|
+
private selectAggressiveMatch;
|
|
23
|
+
/**
|
|
24
|
+
* Get all matches above threshold for a strategy
|
|
25
|
+
*/
|
|
26
|
+
getAllMatchesAboveThreshold(candidates: MatchResult[], strategy: MatchStrategy): MatchResult[];
|
|
27
|
+
/**
|
|
28
|
+
* Get the threshold for a strategy
|
|
29
|
+
*/
|
|
30
|
+
getThreshold(strategy: MatchStrategy): number;
|
|
31
|
+
/**
|
|
32
|
+
* Check if a score meets the strategy threshold
|
|
33
|
+
*/
|
|
34
|
+
meetsThreshold(score: number, strategy: MatchStrategy): boolean;
|
|
35
|
+
}
|