@memberjunction/actions-apollo 2.79.0 → 2.80.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/README.md +106 -82
- package/dist/accounts.d.ts +321 -11
- package/dist/accounts.d.ts.map +1 -1
- package/dist/accounts.js +786 -366
- package/dist/accounts.js.map +1 -1
- package/dist/contacts.d.ts +232 -8
- package/dist/contacts.d.ts.map +1 -1
- package/dist/contacts.js +441 -144
- package/dist/contacts.js.map +1 -1
- package/dist/generic/apollo.types.d.ts +64 -60
- package/dist/generic/apollo.types.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/contacts.js
CHANGED
|
@@ -32,153 +32,380 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
32
32
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33
33
|
};
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
-
exports.LoadApolloContactsEnrichmentAction = exports.
|
|
35
|
+
exports.LoadApolloContactsEnrichmentAction = exports.ApolloEnrichmentContactsAction = void 0;
|
|
36
36
|
const actions_1 = require("@memberjunction/actions");
|
|
37
37
|
const global_1 = require("@memberjunction/global");
|
|
38
38
|
const Config = __importStar(require("./config"));
|
|
39
39
|
const core_1 = require("@memberjunction/core");
|
|
40
40
|
const axios_1 = __importDefault(require("axios"));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Action that enriches contact records using Apollo.io's people matching and search APIs
|
|
43
|
+
*
|
|
44
|
+
* This action performs contact enrichment by:
|
|
45
|
+
* 1. Bulk matching existing contacts against Apollo's database
|
|
46
|
+
* 2. Updating contact records with enriched social profiles and company data
|
|
47
|
+
* 3. Creating employment and education history records
|
|
48
|
+
* 4. Processing contacts in configurable batch sizes for optimal performance
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* // Basic contact enrichment
|
|
53
|
+
* await runAction({
|
|
54
|
+
* ActionName: 'ApolloEnrichmentContactsAction',
|
|
55
|
+
* Params: [{
|
|
56
|
+
* Name: 'EntityName',
|
|
57
|
+
* Value: 'Contacts'
|
|
58
|
+
* }, {
|
|
59
|
+
* Name: 'EmailField',
|
|
60
|
+
* Value: 'Email'
|
|
61
|
+
* }, {
|
|
62
|
+
* Name: 'FirstNameField',
|
|
63
|
+
* Value: 'FirstName'
|
|
64
|
+
* }, {
|
|
65
|
+
* Name: 'LastNameField',
|
|
66
|
+
* Value: 'LastName'
|
|
67
|
+
* }, {
|
|
68
|
+
* Name: 'TitleField',
|
|
69
|
+
* Value: 'Title'
|
|
70
|
+
* }, {
|
|
71
|
+
* Name: 'EnrichedAtField',
|
|
72
|
+
* Value: 'EnrichedAt'
|
|
73
|
+
* }, {
|
|
74
|
+
* Name: 'Filter',
|
|
75
|
+
* Value: 'Email IS NOT NULL AND EnrichedAt IS NULL'
|
|
76
|
+
* }]
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* // Full enrichment with history tracking
|
|
80
|
+
* await runAction({
|
|
81
|
+
* ActionName: 'ApolloEnrichmentContactsAction',
|
|
82
|
+
* Params: [
|
|
83
|
+
* // ... basic params ...
|
|
84
|
+
* {
|
|
85
|
+
* Name: 'EmploymentHistoryFieldMappings',
|
|
86
|
+
* Value: JSON.stringify({
|
|
87
|
+
* EmploymentHistoryEntityName: 'Contact Employment Histories',
|
|
88
|
+
* EmploymentHistoryContactIDFieldName: 'ContactID',
|
|
89
|
+
* EmploymentHistoryOrganizationFieldName: 'Organization',
|
|
90
|
+
* EmploymentHistoryTitleFieldName: 'Title'
|
|
91
|
+
* })
|
|
92
|
+
* }
|
|
93
|
+
* ]
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
let ApolloEnrichmentContactsAction = class ApolloEnrichmentContactsAction extends actions_1.BaseAction {
|
|
53
98
|
constructor() {
|
|
54
99
|
super(...arguments);
|
|
100
|
+
/**
|
|
101
|
+
* Job titles to exclude when processing contacts
|
|
102
|
+
* These are typically not business decision-makers
|
|
103
|
+
*/
|
|
55
104
|
this.ExcludeTitles = ['member', 'student member', 'student', 'volunteer'];
|
|
56
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Main entry point for the Apollo contact enrichment action
|
|
108
|
+
*
|
|
109
|
+
* @param params - The action parameters containing:
|
|
110
|
+
* - EntityName: Required - Name of the contact entity to enrich
|
|
111
|
+
* - EmailField: Required - Field name containing email addresses
|
|
112
|
+
* - FirstNameField: Required - Field name containing first names
|
|
113
|
+
* - LastNameField: Required - Field name containing last names
|
|
114
|
+
* - TitleField: Required - Field name containing job titles
|
|
115
|
+
* - EnrichedAtField: Required - Field to track enrichment timestamp
|
|
116
|
+
* - Filter: Required - SQL filter to select contacts for enrichment
|
|
117
|
+
* - ProfilePictureURLField: Optional - Field for profile picture URLs
|
|
118
|
+
* - AccountNameField: Optional - Field for company/account names
|
|
119
|
+
* - DomainField: Optional - Field for email domains
|
|
120
|
+
* - LinkedInField: Optional - Field for LinkedIn profile URLs
|
|
121
|
+
* - TwitterField: Optional - Field for Twitter profile URLs
|
|
122
|
+
* - FacebookField: Optional - Field for Facebook profile URLs
|
|
123
|
+
* - EmploymentHistoryFieldMappings: Optional - JSON mapping for employment history entity
|
|
124
|
+
* - EducationHistoryFieldMappings: Optional - JSON mapping for education history entity
|
|
125
|
+
*
|
|
126
|
+
* @returns Promise<ActionResultSimple> indicating success/failure and any error messages
|
|
127
|
+
*/
|
|
57
128
|
async InternalRunAction(params) {
|
|
58
|
-
|
|
59
|
-
|
|
129
|
+
try {
|
|
130
|
+
// Validate API configuration
|
|
131
|
+
const configValidation = this.validateConfiguration();
|
|
132
|
+
if (!configValidation.success) {
|
|
133
|
+
return configValidation.error;
|
|
134
|
+
}
|
|
135
|
+
// Extract and validate parameters
|
|
136
|
+
const paramValidation = this.extractAndValidateParameters(params);
|
|
137
|
+
if (!paramValidation.success) {
|
|
138
|
+
return paramValidation.error;
|
|
139
|
+
}
|
|
140
|
+
const { actionParams, historyMappings } = paramValidation.data;
|
|
141
|
+
// Process all contact records
|
|
142
|
+
return await this.processAllContactRecords(actionParams, historyMappings, params.ContextUser);
|
|
60
143
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const domainParam = params.Params.find(p => p.Name === 'domainParam');
|
|
69
|
-
const linkedinParam = params.Params.find(p => p.Name === 'linkedinParam');
|
|
70
|
-
const EnrichmentFieldMappingParam = params.Params.find(p => p.Name === 'EnrichmentFieldMappings');
|
|
71
|
-
const EducationHistoryEntityNameParam = params.Params.find(p => p.Name === 'EducationHistoryFieldMappings');
|
|
72
|
-
const EmploymentHistoryEntityNameParam = params.Params.find(p => p.Name === 'EmploymentHistoryFieldMappings');
|
|
73
|
-
const educationHistoryFieldMappings = EducationHistoryEntityNameParam ? EducationHistoryEntityNameParam.Value : {};
|
|
74
|
-
const employmentHistoryFieldMappings = EmploymentHistoryEntityNameParam ? EmploymentHistoryEntityNameParam.Value : {};
|
|
75
|
-
if (!entityNameParam) {
|
|
76
|
-
throw new Error("EntityName parameter not found");
|
|
144
|
+
catch (error) {
|
|
145
|
+
(0, core_1.LogError)('Unexpected error in ApolloContactsEnrichmentAction', undefined, error);
|
|
146
|
+
return {
|
|
147
|
+
Success: false,
|
|
148
|
+
Message: `Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
149
|
+
ResultCode: "FAILED"
|
|
150
|
+
};
|
|
77
151
|
}
|
|
78
|
-
|
|
79
|
-
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validates that the Apollo.io API key is configured properly
|
|
155
|
+
* @returns Validation result with success flag and potential error details
|
|
156
|
+
*/
|
|
157
|
+
validateConfiguration() {
|
|
158
|
+
if (!Config.ApolloAPIKey) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: {
|
|
162
|
+
Success: false,
|
|
163
|
+
Message: 'Apollo.io API key not found in configuration',
|
|
164
|
+
ResultCode: "CONFIGURATION_ERROR"
|
|
165
|
+
}
|
|
166
|
+
};
|
|
80
167
|
}
|
|
81
|
-
|
|
82
|
-
|
|
168
|
+
return { success: true };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Extracts and validates all action parameters from the request
|
|
172
|
+
* Processes both required and optional parameters for contact enrichment
|
|
173
|
+
*
|
|
174
|
+
* @param params - The action parameters containing entity and field configurations
|
|
175
|
+
* @returns Object with success flag, validated parameters, or error details
|
|
176
|
+
*/
|
|
177
|
+
extractAndValidateParameters(params) {
|
|
178
|
+
// Extract required parameters
|
|
179
|
+
const entityName = this.getParamValue(params, 'EntityName');
|
|
180
|
+
const emailField = this.getParamValue(params, 'EmailField');
|
|
181
|
+
const firstNameField = this.getParamValue(params, 'FirstNameField');
|
|
182
|
+
const lastNameField = this.getParamValue(params, 'LastNameField');
|
|
183
|
+
const titleField = this.getParamValue(params, 'TitleField');
|
|
184
|
+
const enrichedAtField = this.getParamValue(params, 'EnrichedAtField');
|
|
185
|
+
const filter = this.getParamValue(params, 'Filter');
|
|
186
|
+
// Check for any missing required parameters
|
|
187
|
+
const missingParams = [];
|
|
188
|
+
if (!entityName)
|
|
189
|
+
missingParams.push('EntityName');
|
|
190
|
+
if (!emailField)
|
|
191
|
+
missingParams.push('EmailField');
|
|
192
|
+
if (!firstNameField)
|
|
193
|
+
missingParams.push('FirstNameField');
|
|
194
|
+
if (!lastNameField)
|
|
195
|
+
missingParams.push('LastNameField');
|
|
196
|
+
if (!titleField)
|
|
197
|
+
missingParams.push('TitleField');
|
|
198
|
+
if (!enrichedAtField)
|
|
199
|
+
missingParams.push('EnrichedAtField');
|
|
200
|
+
if (!filter)
|
|
201
|
+
missingParams.push('Filter');
|
|
202
|
+
if (missingParams.length > 0) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: {
|
|
206
|
+
Success: false,
|
|
207
|
+
Message: `Missing required parameters: ${missingParams.join(', ')}`,
|
|
208
|
+
ResultCode: "VALIDATION_ERROR"
|
|
209
|
+
}
|
|
210
|
+
};
|
|
83
211
|
}
|
|
84
|
-
|
|
85
|
-
|
|
212
|
+
// Extract optional parameters
|
|
213
|
+
const profilePictureURLField = this.getParamValue(params, 'ProfilePictureURLField');
|
|
214
|
+
const accountNameField = this.getParamValue(params, 'AccountNameField');
|
|
215
|
+
const domainField = this.getParamValue(params, 'DomainField');
|
|
216
|
+
const linkedInField = this.getParamValue(params, 'LinkedInField');
|
|
217
|
+
const twitterField = this.getParamValue(params, 'TwitterField');
|
|
218
|
+
const facebookField = this.getParamValue(params, 'FacebookField');
|
|
219
|
+
// Extract history mappings
|
|
220
|
+
const educationHistoryMappings = this.parseHistoryMappings(params, 'EducationHistoryFieldMappings');
|
|
221
|
+
const employmentHistoryMappings = this.parseHistoryMappings(params, 'EmploymentHistoryFieldMappings');
|
|
222
|
+
const actionParams = {
|
|
223
|
+
EntityName: entityName,
|
|
224
|
+
EmailField: emailField,
|
|
225
|
+
FirstNameField: firstNameField,
|
|
226
|
+
LastNameField: lastNameField,
|
|
227
|
+
TitleField: titleField,
|
|
228
|
+
EnrichedAtField: enrichedAtField,
|
|
229
|
+
Filter: filter,
|
|
230
|
+
ProfilePictureURLField: profilePictureURLField,
|
|
231
|
+
AccountNameField: accountNameField,
|
|
232
|
+
DomainField: domainField,
|
|
233
|
+
LinkedInField: linkedInField,
|
|
234
|
+
TwitterField: twitterField,
|
|
235
|
+
FacebookField: facebookField
|
|
236
|
+
};
|
|
237
|
+
const historyMappings = {
|
|
238
|
+
education: educationHistoryMappings,
|
|
239
|
+
employment: employmentHistoryMappings
|
|
240
|
+
};
|
|
241
|
+
return { success: true, data: { actionParams, historyMappings } };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Extract parameter value by name (case-insensitive)
|
|
245
|
+
* Handles special case where 'null' string should be treated as null value
|
|
246
|
+
*
|
|
247
|
+
* @param params - The action parameters
|
|
248
|
+
* @param paramName - Name of the parameter to extract
|
|
249
|
+
* @returns Parameter value or null if not found or explicitly 'null'
|
|
250
|
+
*/
|
|
251
|
+
getParamValue(params, paramName) {
|
|
252
|
+
const param = params.Params.find(p => p.Name.trim().toLowerCase() === paramName.toLowerCase());
|
|
253
|
+
if (param && param.Value && param.Value === 'null') {
|
|
254
|
+
return null;
|
|
86
255
|
}
|
|
87
|
-
|
|
88
|
-
|
|
256
|
+
else {
|
|
257
|
+
return param?.Value;
|
|
89
258
|
}
|
|
90
|
-
|
|
91
|
-
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Parses JSON field mappings for employment/education history entities
|
|
262
|
+
* Used to configure how history records should be created and updated
|
|
263
|
+
*
|
|
264
|
+
* @param params - The action parameters
|
|
265
|
+
* @param paramName - Name of the history mapping parameter
|
|
266
|
+
* @returns Parsed field mappings or empty object if invalid/missing
|
|
267
|
+
*/
|
|
268
|
+
parseHistoryMappings(params, paramName) {
|
|
269
|
+
const param = params.Params.find(p => p.Name === paramName);
|
|
270
|
+
if (!param || !param.Value) {
|
|
271
|
+
return {};
|
|
92
272
|
}
|
|
93
|
-
|
|
94
|
-
|
|
273
|
+
// If it's already an object, use it directly
|
|
274
|
+
if (typeof param.Value === 'object' && param.Value !== null) {
|
|
275
|
+
return param.Value;
|
|
95
276
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Message: runViewResult.ErrorMessage,
|
|
106
|
-
ResultCode: "FAILED"
|
|
107
|
-
};
|
|
277
|
+
// If it's a string, try to parse it as JSON
|
|
278
|
+
if (typeof param.Value === 'string') {
|
|
279
|
+
try {
|
|
280
|
+
return JSON.parse(param.Value) || {};
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
(0, core_1.LogError)(`Error parsing ${paramName}`, undefined, error);
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
108
286
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
287
|
+
return {};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Processes all contact records using paginated queries
|
|
291
|
+
* Continues processing until all matching records have been enriched
|
|
292
|
+
*
|
|
293
|
+
* @param actionParams - Configuration parameters for contact enrichment
|
|
294
|
+
* @param historyMappings - Field mappings for employment/education history
|
|
295
|
+
* @param contextUser - User context for data access and security
|
|
296
|
+
* @returns Promise indicating overall success/failure of the enrichment
|
|
297
|
+
*/
|
|
298
|
+
async processAllContactRecords(actionParams, historyMappings, contextUser) {
|
|
112
299
|
let pageNumber = 0;
|
|
113
300
|
let hasMore = true;
|
|
301
|
+
const md = new core_1.Metadata();
|
|
114
302
|
while (hasMore) {
|
|
115
303
|
(0, core_1.LogStatus)(`Fetching page ${pageNumber + 1} of records...`);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
PageSize: 500,
|
|
120
|
-
Filter: `ID IN (select RecordID from __mj.vwListDetails WHERE ListID = '4C04EEF4-7970-EF11-BDFD-00224879D6C4')
|
|
121
|
-
AND ID NOT IN (SELECT DISTINCT PersonID from ATD.PersonEmploymentHistory)`
|
|
122
|
-
};
|
|
123
|
-
const results = await this.PageRecordsByEntityName(config, params.ContextUser);
|
|
124
|
-
if (!results) {
|
|
125
|
-
return {
|
|
126
|
-
Success: false,
|
|
127
|
-
Message: "Error querying entity records",
|
|
128
|
-
ResultCode: "FAILED"
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
(0, core_1.LogStatus)(`Fetched ${results.length} records with max page size of ${config.PageSize}`);
|
|
132
|
-
resultLength = results.length;
|
|
133
|
-
const tasks = []; // to hold promises
|
|
134
|
-
for (let i = 0; i < results.length; i += Config.GroupSize) {
|
|
135
|
-
// push the promise into tasks array without awaiting
|
|
136
|
-
let processParams = {
|
|
137
|
-
Records: results,
|
|
138
|
-
Startrow: i,
|
|
139
|
-
GroupLength: Config.GroupSize,
|
|
140
|
-
Md: md,
|
|
141
|
-
CurrentUser: params.ContextUser,
|
|
142
|
-
EmailField: emailFieldParam.Value,
|
|
143
|
-
FirstNameField: firstNameFieldParam.Value,
|
|
144
|
-
LastNameField: lastNameFieldParam.Value,
|
|
145
|
-
AccountNameField: accountNameFieldParam.Value,
|
|
146
|
-
EntityName: entityNameParam.Value,
|
|
147
|
-
EnrichedAtField: enrichedAtFieldParam.Value,
|
|
148
|
-
DomainField: domainParam?.Value,
|
|
149
|
-
LinkedInField: linkedinParam?.Value,
|
|
150
|
-
EmploymentHistoryEntityName: employmentHistoryFieldMappings.EmploymentHistoryEntityName,
|
|
151
|
-
EmploymentHistoryContactIDFieldName: employmentHistoryFieldMappings.EmploymentHistoryContactIDFieldName,
|
|
152
|
-
EmploymentHistoryOrganizationFieldName: employmentHistoryFieldMappings.EmploymentHistoryOrganizationFieldName,
|
|
153
|
-
EmploymentHistoryTitleFieldName: employmentHistoryFieldMappings.EmploymentHistoryTitleFieldName,
|
|
154
|
-
EducationHistoryEntityName: educationHistoryFieldMappings.EducationHistoryEntityName,
|
|
155
|
-
EducationHistoryContactIDFieldName: educationHistoryFieldMappings.EducationtHistoryContactIDFieldName,
|
|
156
|
-
EducationHistoryInstitutionFieldName: educationHistoryFieldMappings.EducationtHistoryInstitutionFieldName,
|
|
157
|
-
EducationHistoryDegreeFieldName: educationHistoryFieldMappings.EducationtHistoryDegreeFieldName
|
|
158
|
-
};
|
|
159
|
-
const task = this.ProcessPersonRecordGroup(processParams);
|
|
160
|
-
tasks.push(task);
|
|
161
|
-
if (tasks.length === Config.ConcurrentGroups || i + 1 >= results.length) {
|
|
162
|
-
// if we started n tasks or reached the end of the result array, await all started tasks
|
|
163
|
-
await Promise.all(tasks);
|
|
164
|
-
// clear tasks
|
|
165
|
-
tasks.length = 0;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (resultLength < config.PageSize) {
|
|
169
|
-
console.log(results.length, config.PageSize);
|
|
170
|
-
(0, core_1.LogStatus)(`No more records to process after this page. Total pages: ${pageNumber + 1}`);
|
|
171
|
-
hasMore = false;
|
|
172
|
-
break;
|
|
304
|
+
const pageResult = await this.processContactRecordsPage(actionParams, historyMappings, pageNumber, md, contextUser);
|
|
305
|
+
if (!pageResult.success) {
|
|
306
|
+
return pageResult.error;
|
|
173
307
|
}
|
|
308
|
+
hasMore = pageResult.hasMore;
|
|
174
309
|
pageNumber++;
|
|
175
310
|
}
|
|
176
311
|
return {
|
|
177
312
|
Success: true,
|
|
178
|
-
Message: "
|
|
313
|
+
Message: "ApolloContactsEnrichment executed successfully.",
|
|
179
314
|
ResultCode: "SUCCESS"
|
|
180
315
|
};
|
|
181
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Processes a single page of contact records for enrichment
|
|
319
|
+
* Retrieves records using pagination and processes them in concurrent groups
|
|
320
|
+
*
|
|
321
|
+
* @param actionParams - Configuration parameters for contact enrichment
|
|
322
|
+
* @param historyMappings - Field mappings for employment/education history
|
|
323
|
+
* @param pageNumber - Current page number (zero-based)
|
|
324
|
+
* @param md - Metadata instance for entity operations
|
|
325
|
+
* @param contextUser - User context for data access
|
|
326
|
+
* @returns Object indicating success, whether more pages exist, and any errors
|
|
327
|
+
*/
|
|
328
|
+
async processContactRecordsPage(actionParams, historyMappings, pageNumber, md, contextUser) {
|
|
329
|
+
const pageConfig = {
|
|
330
|
+
EntityName: actionParams.EntityName,
|
|
331
|
+
PageNumber: pageNumber,
|
|
332
|
+
PageSize: 500,
|
|
333
|
+
Filter: actionParams.Filter
|
|
334
|
+
};
|
|
335
|
+
const results = await this.PageRecordsByEntityName(pageConfig, contextUser);
|
|
336
|
+
if (!results) {
|
|
337
|
+
return {
|
|
338
|
+
success: false,
|
|
339
|
+
hasMore: false,
|
|
340
|
+
error: {
|
|
341
|
+
Success: false,
|
|
342
|
+
Message: "Error querying entity records",
|
|
343
|
+
ResultCode: "FAILED"
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
(0, core_1.LogStatus)(`Fetched ${results.length} records with max page size of ${pageConfig.PageSize}`);
|
|
348
|
+
// Process records in concurrent groups
|
|
349
|
+
await this.processRecordGroups(results, actionParams, historyMappings, md, contextUser);
|
|
350
|
+
const hasMore = results.length >= pageConfig.PageSize;
|
|
351
|
+
return { success: true, hasMore };
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Processes contact records in concurrent groups for optimal performance
|
|
355
|
+
* Respects configured group size and concurrency limits
|
|
356
|
+
*
|
|
357
|
+
* @param results - Array of contact records to process
|
|
358
|
+
* @param actionParams - Configuration parameters for enrichment
|
|
359
|
+
* @param historyMappings - Field mappings for history entities
|
|
360
|
+
* @param md - Metadata instance for entity operations
|
|
361
|
+
* @param contextUser - User context for data access
|
|
362
|
+
*/
|
|
363
|
+
async processRecordGroups(results, actionParams, historyMappings, md, contextUser) {
|
|
364
|
+
const tasks = [];
|
|
365
|
+
for (let i = 0; i < results.length; i += Config.GroupSize) {
|
|
366
|
+
const processParams = {
|
|
367
|
+
Records: results,
|
|
368
|
+
Startrow: i,
|
|
369
|
+
GroupLength: Config.GroupSize,
|
|
370
|
+
Md: md,
|
|
371
|
+
CurrentUser: contextUser,
|
|
372
|
+
EmailField: actionParams.EmailField,
|
|
373
|
+
FirstNameField: actionParams.FirstNameField,
|
|
374
|
+
LastNameField: actionParams.LastNameField,
|
|
375
|
+
TitleField: actionParams.TitleField,
|
|
376
|
+
EntityName: actionParams.EntityName,
|
|
377
|
+
EnrichedAtField: actionParams.EnrichedAtField,
|
|
378
|
+
ProfilePictureURLField: actionParams.ProfilePictureURLField,
|
|
379
|
+
AccountNameField: actionParams.AccountNameField,
|
|
380
|
+
DomainField: actionParams.DomainField,
|
|
381
|
+
LinkedInField: actionParams.LinkedInField,
|
|
382
|
+
TwitterField: actionParams.TwitterField,
|
|
383
|
+
FacebookField: actionParams.FacebookField,
|
|
384
|
+
EmploymentHistoryEntityName: historyMappings.employment.EntityName,
|
|
385
|
+
EmploymentHistoryContactIDFieldName: historyMappings.employment.ContactIDFieldName,
|
|
386
|
+
EmploymentHistoryOrganizationFieldName: historyMappings.employment.OrganizationFieldName,
|
|
387
|
+
EmploymentHistoryTitleFieldName: historyMappings.employment.TitleFieldName,
|
|
388
|
+
EducationHistoryEntityName: historyMappings.education.EntityName,
|
|
389
|
+
EducationHistoryContactIDFieldName: historyMappings.education.ContactIDFieldName,
|
|
390
|
+
EducationHistoryInstitutionFieldName: historyMappings.education.InstitutionFieldName,
|
|
391
|
+
EducationHistoryDegreeFieldName: historyMappings.education.DegreeFieldName
|
|
392
|
+
};
|
|
393
|
+
const task = this.ProcessPersonRecordGroup(processParams);
|
|
394
|
+
tasks.push(task);
|
|
395
|
+
if (tasks.length === Config.ConcurrentGroups || i + Config.GroupSize >= results.length) {
|
|
396
|
+
await Promise.all(tasks);
|
|
397
|
+
tasks.length = 0;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Retrieves a page of records from a specified entity using RunView
|
|
403
|
+
* Supports pagination with configurable page size and filtering
|
|
404
|
+
*
|
|
405
|
+
* @param params - Parameters defining entity, page, and filter criteria
|
|
406
|
+
* @param currentUser - User context for data access
|
|
407
|
+
* @returns Array of records or null if query failed
|
|
408
|
+
*/
|
|
182
409
|
async PageRecordsByEntityName(params, currentUser) {
|
|
183
410
|
const rv = new core_1.RunView();
|
|
184
411
|
const rvResult = await rv.RunView({
|
|
@@ -193,13 +420,20 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
193
420
|
}
|
|
194
421
|
return rvResult.Results;
|
|
195
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Processes a group of contact records using Apollo's bulk people matching API
|
|
425
|
+
* Enriches contacts with social profiles, company data, and employment history
|
|
426
|
+
*
|
|
427
|
+
* @param params - Group processing parameters with contact data and entity mappings
|
|
428
|
+
* @returns Promise<boolean> indicating success/failure of the group processing
|
|
429
|
+
*/
|
|
196
430
|
async ProcessPersonRecordGroup(params) {
|
|
197
431
|
try {
|
|
198
432
|
const md = params.Md;
|
|
199
433
|
const ApolloParams = {
|
|
200
434
|
api_key: Config.ApolloAPIKey,
|
|
201
435
|
reveal_personal_emails: true,
|
|
202
|
-
details: params.Records.
|
|
436
|
+
details: params.Records.slice(params.Startrow, params.Startrow + params.GroupLength).map((record) => {
|
|
203
437
|
const first_name = record[params.FirstNameField];
|
|
204
438
|
const last_name = record[params.LastNameField];
|
|
205
439
|
const email = record[params.EmailField];
|
|
@@ -226,40 +460,40 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
226
460
|
(0, core_1.LogStatus)('No match found for record at index ' + params.Startrow + index);
|
|
227
461
|
continue;
|
|
228
462
|
}
|
|
229
|
-
const contactEntity = await md.GetEntityObject(params.EntityName, params.CurrentUser);
|
|
230
463
|
const email = match.email;
|
|
231
464
|
if (email && email.trim().length > 0) {
|
|
232
465
|
// update the contact record
|
|
233
466
|
let entityRecord = params.Records[index + params.Startrow];
|
|
234
467
|
if (!entityRecord) {
|
|
235
468
|
(0, core_1.LogError)('Error updating contact record with enriched data, entity record not found', undefined, entityRecord);
|
|
469
|
+
bSuccess = false;
|
|
236
470
|
continue;
|
|
237
471
|
}
|
|
238
|
-
contactEntity.
|
|
472
|
+
const contactEntity = await md.GetEntityObject(params.EntityName, core_1.CompositeKey.FromID(entityRecord.ID), params.CurrentUser);
|
|
239
473
|
contactEntity.Set(params.EmailField, match.email);
|
|
240
474
|
contactEntity.Set(params.EnrichedAtField, new Date());
|
|
241
|
-
if (match.
|
|
475
|
+
if (params.TitleField && match.title) {
|
|
476
|
+
contactEntity.Set(params.TitleField, match.title);
|
|
477
|
+
}
|
|
478
|
+
if (params.AccountNameField && match.organization && match.organization.name) {
|
|
242
479
|
contactEntity.Set(params.AccountNameField, match.organization.name);
|
|
243
480
|
}
|
|
244
|
-
if (params.
|
|
481
|
+
if (params.ProfilePictureURLField && match.photo_url) {
|
|
482
|
+
contactEntity.Set(params.ProfilePictureURLField, match.photo_url);
|
|
483
|
+
}
|
|
484
|
+
if (params.LinkedInField && match.linkedin_url) {
|
|
245
485
|
contactEntity.Set(params.LinkedInField, match.linkedin_url);
|
|
246
486
|
}
|
|
247
|
-
if (
|
|
248
|
-
contactEntity.Set(params.
|
|
487
|
+
if (params.TwitterField && match.twitter_url) {
|
|
488
|
+
contactEntity.Set(params.TwitterField, match.twitter_url);
|
|
489
|
+
}
|
|
490
|
+
if (params.FacebookField && match.facebook_url) {
|
|
491
|
+
contactEntity.Set(params.FacebookField, match.facebook_url);
|
|
249
492
|
}
|
|
250
493
|
bSuccess = bSuccess && (await contactEntity.Save());
|
|
251
494
|
if (!bSuccess) {
|
|
252
495
|
(0, core_1.LogError)('Error updating contact record with enriched data', undefined, contactEntity.LatestResult);
|
|
253
496
|
}
|
|
254
|
-
/*
|
|
255
|
-
if(match.organization_id){
|
|
256
|
-
LogStatus(`Found organization: ${match.organization?.name}`);
|
|
257
|
-
await this.UpsertContactEmploymentAndEducationHistoryByOrganizationID(match.organization_id, params);
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
LogStatus('No organization ID found in match');
|
|
261
|
-
}
|
|
262
|
-
*/
|
|
263
497
|
if (match.employment_history && params.EmploymentHistoryEntityName) {
|
|
264
498
|
(0, core_1.LogStatus)('Upserting contact employment and education history...');
|
|
265
499
|
await this.UpsertContactEmploymentAndEducationHistory(match, contactEntity, params);
|
|
@@ -296,6 +530,13 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
296
530
|
return false;
|
|
297
531
|
}
|
|
298
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Enriches contacts by searching for people at a specific organization domain
|
|
535
|
+
* Uses Apollo's people search to find contacts and update their employment/education history
|
|
536
|
+
*
|
|
537
|
+
* @param domain - Organization domain to search for people
|
|
538
|
+
* @param params - Processing parameters with entity configurations
|
|
539
|
+
*/
|
|
299
540
|
async UpsertContactEmploymentAndEducationHistoryByOrganizationID(domain, params) {
|
|
300
541
|
try {
|
|
301
542
|
const api_params = {
|
|
@@ -363,7 +604,6 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
363
604
|
continue;
|
|
364
605
|
}
|
|
365
606
|
const contactEntity = await params.Md.GetEntityObject(params.EntityName, params.CurrentUser);
|
|
366
|
-
contactEntity.SetMany(rvContactResults.Results[0]);
|
|
367
607
|
await this.UpsertContactEmploymentAndEducationHistory(p, contactEntity, params);
|
|
368
608
|
}
|
|
369
609
|
i++;
|
|
@@ -378,6 +618,14 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
378
618
|
(0, core_1.LogError)('Error upserting contact education and employment history by organization ID: ', undefined, ex);
|
|
379
619
|
}
|
|
380
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Creates or updates employment and education history records for a contact
|
|
623
|
+
* Processes Apollo employment history data to create both employment and education records
|
|
624
|
+
*
|
|
625
|
+
* @param contact - Apollo person data containing employment history
|
|
626
|
+
* @param contactEntity - Contact entity to create history for
|
|
627
|
+
* @param params - Processing parameters with history entity configurations
|
|
628
|
+
*/
|
|
381
629
|
async UpsertContactEmploymentAndEducationHistory(contact, contactEntity, params) {
|
|
382
630
|
try {
|
|
383
631
|
// loop through the person.employment_history array and create/update the employment history records
|
|
@@ -400,12 +648,13 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
400
648
|
continue;
|
|
401
649
|
}
|
|
402
650
|
const results = rvResults.Results;
|
|
403
|
-
|
|
651
|
+
let historyEntity = null;
|
|
404
652
|
if (results.length > 0) {
|
|
405
653
|
// update the existing record
|
|
406
|
-
historyEntity.
|
|
654
|
+
historyEntity = await params.Md.GetEntityObject(params.EmploymentHistoryEntityName, core_1.CompositeKey.FromID(results[0].ID), params.CurrentUser);
|
|
407
655
|
}
|
|
408
656
|
else {
|
|
657
|
+
historyEntity = await params.Md.GetEntityObject(params.EmploymentHistoryEntityName, params.CurrentUser);
|
|
409
658
|
historyEntity.NewRecord();
|
|
410
659
|
historyEntity.Set(params.EmploymentHistoryContactIDFieldName, contactID);
|
|
411
660
|
historyEntity.Set("CreatedAt", new Date());
|
|
@@ -416,7 +665,7 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
416
665
|
historyEntity.Set('StartedAt', employment.start_date);
|
|
417
666
|
}
|
|
418
667
|
if (this.IsValidDate(employment.end_date)) {
|
|
419
|
-
historyEntity.Set('EndedAt', employment.
|
|
668
|
+
historyEntity.Set('EndedAt', employment.end_date);
|
|
420
669
|
}
|
|
421
670
|
historyEntity.Set("UpdatedAt", new Date());
|
|
422
671
|
historyEntity.Set("IsCurrent", employment.current);
|
|
@@ -440,12 +689,13 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
440
689
|
continue;
|
|
441
690
|
}
|
|
442
691
|
const educationResults = rvEducationResults.Results;
|
|
443
|
-
|
|
692
|
+
let educationEntity = null;
|
|
444
693
|
if (educationResults.length > 0) {
|
|
445
694
|
// update the existing record
|
|
446
|
-
educationEntity.
|
|
695
|
+
educationEntity = await params.Md.GetEntityObject(params.EducationHistoryEntityName, core_1.CompositeKey.FromID(educationResults[0].ID), params.CurrentUser);
|
|
447
696
|
}
|
|
448
697
|
else {
|
|
698
|
+
educationEntity = await params.Md.GetEntityObject(params.EducationHistoryEntityName, params.CurrentUser);
|
|
449
699
|
educationEntity.NewRecord();
|
|
450
700
|
educationEntity.Set(params.EducationHistoryContactIDFieldName, contactID);
|
|
451
701
|
educationEntity.Set("CreatedAt", new Date());
|
|
@@ -459,7 +709,7 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
459
709
|
educationEntity.Set('StartedAt', employment.start_date);
|
|
460
710
|
}
|
|
461
711
|
if (this.IsValidDate(employment.end_date)) {
|
|
462
|
-
educationEntity.Set('EndedAt', employment.
|
|
712
|
+
educationEntity.Set('EndedAt', employment.end_date);
|
|
463
713
|
}
|
|
464
714
|
const saveResult = await educationEntity.Save();
|
|
465
715
|
if (!saveResult) {
|
|
@@ -472,12 +722,26 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
472
722
|
(0, core_1.LogError)('Error upserting contact education and employment history: ', undefined, ex);
|
|
473
723
|
}
|
|
474
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Escapes single quotes in strings for SQL query safety
|
|
727
|
+
* Prevents SQL injection in dynamic filter construction
|
|
728
|
+
*
|
|
729
|
+
* @param str - String to escape
|
|
730
|
+
* @returns Escaped string with doubled single quotes
|
|
731
|
+
*/
|
|
475
732
|
EscapeSingleQuotes(str) {
|
|
476
733
|
if (str) {
|
|
477
734
|
return str.replace(/'/g, "''");
|
|
478
735
|
}
|
|
479
736
|
return '';
|
|
480
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Validates if a date string represents a valid date
|
|
740
|
+
* Used for employment/education history date validation
|
|
741
|
+
*
|
|
742
|
+
* @param dateString - Date string to validate
|
|
743
|
+
* @returns boolean - true if valid date
|
|
744
|
+
*/
|
|
481
745
|
IsValidDate(dateString) {
|
|
482
746
|
if (dateString && dateString.length > 0) {
|
|
483
747
|
const date = new Date(dateString);
|
|
@@ -485,6 +749,18 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
485
749
|
}
|
|
486
750
|
return false;
|
|
487
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* Wraps Apollo.io API calls with intelligent retry logic for rate limiting
|
|
754
|
+
* Handles both per-minute and hourly rate limits with appropriate backoff
|
|
755
|
+
*
|
|
756
|
+
* @param method - HTTP method ('get' or 'post')
|
|
757
|
+
* @param endpoint - Apollo API endpoint path
|
|
758
|
+
* @param data - Request payload for POST requests
|
|
759
|
+
* @param config - Axios configuration object
|
|
760
|
+
* @param retryAttempts - Number of retry attempts remaining (default: 1)
|
|
761
|
+
* @returns Promise<AxiosResponse> - Apollo API response
|
|
762
|
+
* @throws Error if rate limit exceeded or other API errors
|
|
763
|
+
*/
|
|
488
764
|
async WrapApolloCall(method, endpoint, data, config, retryAttempts = 1) {
|
|
489
765
|
try {
|
|
490
766
|
const url = `${Config.ApolloAPIEndpoint}/${endpoint}`;
|
|
@@ -507,7 +783,9 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
507
783
|
let waitTime = 60000; // default to 1 minute
|
|
508
784
|
//this is a rough guesstimate, check the response headers for more accurate info
|
|
509
785
|
if (apolloError.response.data) {
|
|
510
|
-
const errorMessage = apolloError.response.data
|
|
786
|
+
const errorMessage = typeof apolloError.response.data === 'string'
|
|
787
|
+
? apolloError.response.data
|
|
788
|
+
: JSON.stringify(apolloError.response.data);
|
|
511
789
|
if (errorMessage.includes('times per hour')) {
|
|
512
790
|
(0, core_1.LogStatus)(" >>> Hourly rate limit reached");
|
|
513
791
|
//we reached out hourly rate limit, so wait an hour
|
|
@@ -528,9 +806,23 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
528
806
|
}
|
|
529
807
|
}
|
|
530
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* Utility method for creating delays in async operations
|
|
811
|
+
* Used for rate limit backoff and API throttling
|
|
812
|
+
*
|
|
813
|
+
* @param ms - Number of milliseconds to wait
|
|
814
|
+
* @returns Promise that resolves after the specified delay
|
|
815
|
+
*/
|
|
531
816
|
async Timeout(ms) {
|
|
532
817
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
533
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* Checks if a job title should be excluded from contact processing
|
|
821
|
+
* Filters out non-business contacts like students and volunteers
|
|
822
|
+
*
|
|
823
|
+
* @param title - Job title to check
|
|
824
|
+
* @returns boolean - true if title should be excluded
|
|
825
|
+
*/
|
|
534
826
|
IsExcludedTitle(title) {
|
|
535
827
|
if (!title) {
|
|
536
828
|
return false;
|
|
@@ -538,11 +830,16 @@ let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extend
|
|
|
538
830
|
return this.ExcludeTitles.includes(title.trim().toLowerCase());
|
|
539
831
|
}
|
|
540
832
|
};
|
|
541
|
-
exports.
|
|
542
|
-
exports.
|
|
543
|
-
(0, global_1.RegisterClass)(actions_1.BaseAction, "
|
|
544
|
-
],
|
|
833
|
+
exports.ApolloEnrichmentContactsAction = ApolloEnrichmentContactsAction;
|
|
834
|
+
exports.ApolloEnrichmentContactsAction = ApolloEnrichmentContactsAction = __decorate([
|
|
835
|
+
(0, global_1.RegisterClass)(actions_1.BaseAction, "ApolloEnrichmentContactsAction")
|
|
836
|
+
], ApolloEnrichmentContactsAction);
|
|
837
|
+
/**
|
|
838
|
+
* Loader function to ensure the ApolloEnrichmentContactsAction class is included in the bundle
|
|
839
|
+
* Prevents tree shaking from removing this action class
|
|
840
|
+
*/
|
|
545
841
|
function LoadApolloContactsEnrichmentAction() {
|
|
842
|
+
// Stub function to prevent tree shaking
|
|
546
843
|
}
|
|
547
844
|
exports.LoadApolloContactsEnrichmentAction = LoadApolloContactsEnrichmentAction;
|
|
548
845
|
//# sourceMappingURL=contacts.js.map
|