@memberjunction/actions-apollo 2.78.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/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.ApolloContactsEnrichmentAction = void 0;
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
- // ApolloEnrichContact this Action would use the www.apollo.io enrichment service and enrich a "contact" type of record. The parameters to this Action would be:
42
- // EntityName - entity in question that contacts "contact" types of records
43
- // EmailField - string - field name in the target entity that contains the email to be used for lookups
44
- // FirstNameField - string
45
- // LastNameField - string
46
- // AccountNameField - string
47
- // EnrichmentFieldMapping - JSON - this JSON string would contain mappings for the wide array of enrichment outputs. The idea is that the outputs could include a wide array of things and this would map any of the scalar values that Apollo.io provides back to entity field names in the target entity
48
- // EducationHistoryEntityName - string - optional - this would contain the name of the entity that will be used to stored the retrieved education history for each contact
49
- // EmploymentHistoryEntityName - string - optional - would contain the name of the entity that will be used to store the retrieved employment history for each contact
50
- // ApolloEnrichAccount - same idea and pattern as above, but would be for Account style entities
51
- // Would appreciate feedback on (a) the approach to how we package this stuff and (b) the approach to params noted above.
52
- let ApolloContactsEnrichmentAction = class ApolloContactsEnrichmentAction extends actions_1.BaseAction {
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
- if (!Config.ApolloAPIKey) {
59
- throw new Error('Apollo.io API key not found');
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
- const entityNameParam = params.Params.find(p => p.Name === 'EntityName');
62
- const emailFieldParam = params.Params.find(p => p.Name === 'EmailField');
63
- const firstNameFieldParam = params.Params.find(p => p.Name === 'FirstNameField');
64
- const lastNameFieldParam = params.Params.find(p => p.Name === 'LastNameField');
65
- const accountNameFieldParam = params.Params.find(p => p.Name === 'FilterParam');
66
- const enrichedAtFieldParam = params.Params.find(p => p.Name === 'EnrichedAtField');
67
- const filterParam = params.Params.find(p => p.Name === 'FilterParam');
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
- if (!emailFieldParam) {
79
- throw new Error("EmailField parameter not found");
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
- if (!firstNameFieldParam) {
82
- throw new Error("FirstNameField parameter not found");
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
- if (!lastNameFieldParam) {
85
- throw new Error("LastNameField parameter not found");
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
- if (!accountNameFieldParam) {
88
- throw new Error("AccountNameField parameter not found");
256
+ else {
257
+ return param?.Value;
89
258
  }
90
- if (!enrichedAtFieldParam) {
91
- throw new Error("EnrichedAtField parameter not found");
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
- if (!filterParam) {
94
- throw new Error("filter parameter not found");
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
- const md = new core_1.Metadata();
97
- const rv = new core_1.RunView();
98
- const runViewResult = await rv.RunView({
99
- EntityName: entityNameParam.Value,
100
- ExtraFilter: filterParam.Value,
101
- }, params.ContextUser);
102
- if (!runViewResult.Success) {
103
- return {
104
- Success: false,
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
- //need a reference to the length because the ProcessPersonRecordGroup function
110
- //mutates the results array
111
- let resultLength = 0;
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
- let config = {
117
- EntityName: entityNameParam.Value,
118
- PageNumber: pageNumber,
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: "ApolloAccountsEnrichment executed successfully.",
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.splice(params.Startrow, params.GroupLength).map((record) => {
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.SetMany(entityRecord, true);
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.organization) {
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.LinkedInField) {
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 (match.organization && match.organization.name) {
248
- contactEntity.Set(params.AccountNameField, match.organization.name);
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
- const historyEntity = await params.Md.GetEntityObject(params.EmploymentHistoryEntityName, params.CurrentUser);
651
+ let historyEntity = null;
404
652
  if (results.length > 0) {
405
653
  // update the existing record
406
- historyEntity.SetMany(results[0]);
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.start_date);
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
- const educationEntity = await params.Md.GetEntityObject(params.EducationHistoryEntityName, params.CurrentUser);
692
+ let educationEntity = null;
444
693
  if (educationResults.length > 0) {
445
694
  // update the existing record
446
- educationEntity.SetMany(educationResults[0]);
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.start_date);
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.ApolloContactsEnrichmentAction = ApolloContactsEnrichmentAction;
542
- exports.ApolloContactsEnrichmentAction = ApolloContactsEnrichmentAction = __decorate([
543
- (0, global_1.RegisterClass)(actions_1.BaseAction, "Apollo Enrichment - Contacts")
544
- ], ApolloContactsEnrichmentAction);
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