@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/accounts.js
CHANGED
|
@@ -32,69 +32,234 @@ 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.LoadApolloAccountsEnrichmentAction = exports.
|
|
35
|
+
exports.LoadApolloAccountsEnrichmentAction = exports.ApolloEnrichmentAccountsAction = 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 account records using Apollo.io's organization and people data
|
|
43
|
+
*
|
|
44
|
+
* This action performs comprehensive account enrichment by:
|
|
45
|
+
* 1. Enriching organization data (address, phone, social profiles, etc.)
|
|
46
|
+
* 2. Creating technology stack records for the organization
|
|
47
|
+
* 3. Finding and enriching contacts associated with the organization
|
|
48
|
+
* 4. Creating employment/education history for contacts
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* // Basic account enrichment
|
|
53
|
+
* await runAction({
|
|
54
|
+
* ActionName: 'ApolloEnrichmentAccountsAction',
|
|
55
|
+
* Params: [{
|
|
56
|
+
* Name: 'AccountEntityFieldMappings',
|
|
57
|
+
* Value: JSON.stringify({
|
|
58
|
+
* EntityName: 'Accounts',
|
|
59
|
+
* Filter: 'Domain IS NOT NULL AND EnrichedAt IS NULL',
|
|
60
|
+
* DomainField: 'Domain',
|
|
61
|
+
* AccountIDField: 'ID',
|
|
62
|
+
* EnrichedAtField: 'EnrichedAt'
|
|
63
|
+
* })
|
|
64
|
+
* }]
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* // Full enrichment with contacts and technologies
|
|
68
|
+
* await runAction({
|
|
69
|
+
* ActionName: 'ApolloEnrichmentAccountsAction',
|
|
70
|
+
* Params: [{
|
|
71
|
+
* Name: 'AccountEntityFieldMappings',
|
|
72
|
+
* Value: JSON.stringify(accountConfig)
|
|
73
|
+
* }, {
|
|
74
|
+
* Name: 'ContactEntityFieldMappings',
|
|
75
|
+
* Value: JSON.stringify(contactConfig)
|
|
76
|
+
* }, {
|
|
77
|
+
* Name: 'AccountTechnologyEntityFieldMappings',
|
|
78
|
+
* Value: JSON.stringify(technologyConfig)
|
|
79
|
+
* }]
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
let ApolloEnrichmentAccountsAction = class ApolloEnrichmentAccountsAction extends actions_1.BaseAction {
|
|
53
84
|
constructor() {
|
|
54
85
|
super(...arguments);
|
|
86
|
+
/**
|
|
87
|
+
* Job titles to exclude when creating contact records
|
|
88
|
+
* These are typically not decision-makers or relevant contacts
|
|
89
|
+
*/
|
|
55
90
|
this.ExcludeTitles = ['member', 'student member', 'student', 'volunteer'];
|
|
56
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Main entry point for the Apollo account enrichment action
|
|
94
|
+
*
|
|
95
|
+
* @param params - The action parameters containing:
|
|
96
|
+
* - AccountEntityFieldMappings: Required JSON configuration for account entity field mappings
|
|
97
|
+
* - ContactEntityFieldMappings: Optional JSON configuration for contact entity field mappings
|
|
98
|
+
* - AccountTechnologyEntityFieldMappings: Optional JSON configuration for technology tracking
|
|
99
|
+
* - TechnologyCategoryEntityFieldMappings: Optional JSON configuration for technology categories
|
|
100
|
+
* - ContactEducationHistoryEntityFieldMappings: Optional JSON configuration for education history
|
|
101
|
+
*
|
|
102
|
+
* @returns Promise<ActionResultSimple> indicating success/failure and any error messages
|
|
103
|
+
*/
|
|
57
104
|
async InternalRunAction(params) {
|
|
105
|
+
try {
|
|
106
|
+
// Validate API configuration
|
|
107
|
+
const configValidation = this.validateConfiguration();
|
|
108
|
+
if (!configValidation.success) {
|
|
109
|
+
return configValidation.error;
|
|
110
|
+
}
|
|
111
|
+
// Extract and validate parameters
|
|
112
|
+
const paramValidation = this.extractAndValidateParameters(params);
|
|
113
|
+
if (!paramValidation.success) {
|
|
114
|
+
return paramValidation.error;
|
|
115
|
+
}
|
|
116
|
+
const actionParams = paramValidation.data;
|
|
117
|
+
// Process all account records
|
|
118
|
+
return await this.processAllAccountRecords(actionParams, params.ContextUser, 1);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
(0, core_1.LogError)('Unexpected error in ApolloAccountsEnrichmentAction', undefined, error);
|
|
122
|
+
return {
|
|
123
|
+
Success: false,
|
|
124
|
+
Message: `Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
125
|
+
ResultCode: "FAILED"
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validates that the Apollo.io API key is configured properly
|
|
131
|
+
* @returns Validation result with success flag and potential error details
|
|
132
|
+
*/
|
|
133
|
+
validateConfiguration() {
|
|
58
134
|
if (!Config.ApolloAPIKey) {
|
|
59
|
-
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
error: {
|
|
138
|
+
Success: false,
|
|
139
|
+
Message: 'Apollo.io API key not found in configuration',
|
|
140
|
+
ResultCode: "CONFIGURATION_ERROR"
|
|
141
|
+
}
|
|
142
|
+
};
|
|
60
143
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (!
|
|
72
|
-
|
|
144
|
+
return { success: true };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Extracts and validates all action parameters from the request
|
|
148
|
+
*
|
|
149
|
+
* @param params - The action parameters containing JSON configurations
|
|
150
|
+
* @returns Object with success flag, validated parameters, or error details
|
|
151
|
+
*/
|
|
152
|
+
extractAndValidateParameters(params) {
|
|
153
|
+
const accountEntityFieldsParam = this.getRequiredParam(params, 'AccountEntityFieldMappings');
|
|
154
|
+
if (!accountEntityFieldsParam) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: {
|
|
158
|
+
Success: false,
|
|
159
|
+
Message: 'Missing required parameter: AccountEntityFieldMappings',
|
|
160
|
+
ResultCode: "VALIDATION_ERROR"
|
|
161
|
+
}
|
|
162
|
+
};
|
|
73
163
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
164
|
+
let accountEntityFields;
|
|
165
|
+
try {
|
|
166
|
+
accountEntityFields = this.parseJSONParam(accountEntityFieldsParam.Value);
|
|
167
|
+
if (!accountEntityFields) {
|
|
168
|
+
throw new Error('Parsed AccountEntityFieldMappings is empty');
|
|
78
169
|
}
|
|
79
170
|
}
|
|
80
|
-
|
|
81
|
-
|
|
171
|
+
catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: {
|
|
175
|
+
Success: false,
|
|
176
|
+
Message: 'Invalid AccountEntityFieldMappings parameter: must be valid JSON or object',
|
|
177
|
+
ResultCode: "VALIDATION_ERROR"
|
|
178
|
+
}
|
|
179
|
+
};
|
|
82
180
|
}
|
|
83
|
-
|
|
84
|
-
|
|
181
|
+
// Extract optional parameters
|
|
182
|
+
const accountTechnologyEntityFields = this.parseOptionalJSONParam(params, 'AccountTechnologyEntityFieldMappings');
|
|
183
|
+
const technologyCategoryEntityFields = this.parseOptionalJSONParam(params, 'TechnologyCategoryEntityFieldMappings');
|
|
184
|
+
const contactEntityFields = this.parseOptionalJSONParam(params, 'ContactEntityFieldMappings');
|
|
185
|
+
const contactEducationHistoryEntityFields = this.parseOptionalJSONParam(params, 'ContactEducationHistoryEntityFieldMappings');
|
|
186
|
+
return {
|
|
187
|
+
success: true,
|
|
188
|
+
data: {
|
|
189
|
+
accountEntityFields,
|
|
190
|
+
accountTechnologyEntityFields,
|
|
191
|
+
technologyCategoryEntityFields,
|
|
192
|
+
contactEntityFields,
|
|
193
|
+
contactEducationHistoryEntityFields
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Gets a required parameter from the action parameters
|
|
199
|
+
* @param params - The action parameters
|
|
200
|
+
* @param paramName - Name of the required parameter
|
|
201
|
+
* @returns The parameter if found, null otherwise
|
|
202
|
+
*/
|
|
203
|
+
getRequiredParam(params, paramName) {
|
|
204
|
+
const param = params.Params.find(p => p.Name === paramName);
|
|
205
|
+
return param || null;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Parses an optional JSON parameter with error handling
|
|
209
|
+
* @param params - The action parameters
|
|
210
|
+
* @param paramName - Name of the optional JSON parameter
|
|
211
|
+
* @returns Parsed object of type T or null if not provided/invalid
|
|
212
|
+
*/
|
|
213
|
+
parseOptionalJSONParam(params, paramName) {
|
|
214
|
+
const param = params.Params.find(p => p.Name === paramName);
|
|
215
|
+
if (!param || !param.Value) {
|
|
216
|
+
return null;
|
|
85
217
|
}
|
|
86
|
-
|
|
87
|
-
|
|
218
|
+
try {
|
|
219
|
+
return this.parseJSONParam(param.Value) || null;
|
|
88
220
|
}
|
|
89
|
-
|
|
90
|
-
|
|
221
|
+
catch (error) {
|
|
222
|
+
(0, core_1.LogError)(`Error parsing ${paramName}`, undefined, error);
|
|
223
|
+
return null;
|
|
91
224
|
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Parses a parameter value that may be a JSON string or already parsed object
|
|
228
|
+
* Handles the case where action parameters ending with JSON can be either strings or objects
|
|
229
|
+
*
|
|
230
|
+
* @param value - The parameter value to parse (string or object)
|
|
231
|
+
* @returns Parsed object of type T
|
|
232
|
+
* @throws Error if value cannot be parsed as JSON (when it's a string)
|
|
233
|
+
*/
|
|
234
|
+
parseJSONParam(value) {
|
|
235
|
+
// If value is already an object (not a string), return it directly
|
|
236
|
+
if (typeof value === 'object' && value !== null) {
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
// If value is a string, try to parse it as JSON
|
|
240
|
+
if (typeof value === 'string') {
|
|
241
|
+
return JSON.parse(value);
|
|
242
|
+
}
|
|
243
|
+
// For other types, try to convert to string first then parse
|
|
244
|
+
return JSON.parse(String(value));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Recursively processes all account records that match the filter criteria
|
|
248
|
+
* Uses batching and concurrency control to handle large datasets efficiently
|
|
249
|
+
*
|
|
250
|
+
* @param actionParams - Validated action parameters with entity field mappings
|
|
251
|
+
* @param contextUser - User context for data access and security
|
|
252
|
+
* @param executionCount - Current execution count to prevent infinite recursion (max 5)
|
|
253
|
+
* @returns Promise indicating overall success/failure of the batch
|
|
254
|
+
*/
|
|
255
|
+
async processAllAccountRecords(actionParams, contextUser, executionCount = 1) {
|
|
92
256
|
const md = new core_1.Metadata();
|
|
93
257
|
const rv = new core_1.RunView();
|
|
258
|
+
// Initial query to get accounts to process
|
|
94
259
|
const runViewResult = await rv.RunView({
|
|
95
|
-
EntityName:
|
|
96
|
-
ExtraFilter:
|
|
97
|
-
},
|
|
260
|
+
EntityName: actionParams.accountEntityFields.EntityName,
|
|
261
|
+
ExtraFilter: actionParams.accountEntityFields.Filter,
|
|
262
|
+
}, contextUser);
|
|
98
263
|
if (!runViewResult.Success) {
|
|
99
264
|
return {
|
|
100
265
|
Success: false,
|
|
@@ -103,226 +268,339 @@ let ApolloAccountsEnrichmentAction = class ApolloAccountsEnrichmentAction extend
|
|
|
103
268
|
};
|
|
104
269
|
}
|
|
105
270
|
const results = runViewResult.Results;
|
|
106
|
-
const resultLength =
|
|
107
|
-
|
|
271
|
+
const resultLength = results.length;
|
|
272
|
+
if (resultLength === 0) {
|
|
273
|
+
(0, core_1.LogStatus)('+++ Completed domain enrichment, no records left to process.');
|
|
274
|
+
return {
|
|
275
|
+
Success: true,
|
|
276
|
+
Message: "ApolloAccountsEnrichment executed successfully.",
|
|
277
|
+
ResultCode: "SUCCESS"
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Check execution limit to prevent infinite recursion
|
|
281
|
+
if (executionCount >= 5) {
|
|
282
|
+
(0, core_1.LogStatus)(`+++ Reached maximum execution limit (${executionCount}), stopping to prevent infinite recursion.`);
|
|
283
|
+
return {
|
|
284
|
+
Success: true,
|
|
285
|
+
Message: `ApolloAccountsEnrichment stopped after ${executionCount} executions to prevent infinite recursion. ${resultLength} records may remain unprocessed.`,
|
|
286
|
+
ResultCode: "SUCCESS"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Process accounts in concurrent groups
|
|
290
|
+
await this.processAccountGroups(results, actionParams, md, contextUser);
|
|
291
|
+
// Check if there are more records to process by running the query again
|
|
292
|
+
(0, core_1.LogStatus)(`--- Checking for remaining records... (execution ${executionCount + 1}/5)`);
|
|
293
|
+
return await this.processAllAccountRecords(actionParams, contextUser, executionCount + 1);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Processes account records in concurrent groups to optimize performance
|
|
297
|
+
* Respects the configured concurrency limit to avoid overwhelming APIs
|
|
298
|
+
*
|
|
299
|
+
* @param results - Array of account records to process
|
|
300
|
+
* @param actionParams - Configuration parameters for enrichment
|
|
301
|
+
* @param md - Metadata instance for entity operations
|
|
302
|
+
* @param contextUser - User context for data access
|
|
303
|
+
*/
|
|
304
|
+
async processAccountGroups(results, actionParams, md, contextUser) {
|
|
305
|
+
const tasks = [];
|
|
306
|
+
const resultLength = results.length;
|
|
108
307
|
for (const [index, record] of results.entries()) {
|
|
109
308
|
const processParams = {
|
|
110
309
|
Record: record,
|
|
111
|
-
AccountEntity:
|
|
112
|
-
AccountTechnologyEntity:
|
|
113
|
-
TechnologyCategoryEntity:
|
|
114
|
-
ContactEntity:
|
|
115
|
-
ContactEducationHistoryEntity:
|
|
310
|
+
AccountEntity: actionParams.accountEntityFields,
|
|
311
|
+
AccountTechnologyEntity: actionParams.accountTechnologyEntityFields || null,
|
|
312
|
+
TechnologyCategoryEntity: actionParams.technologyCategoryEntityFields || null,
|
|
313
|
+
ContactEntity: actionParams.contactEntityFields || null,
|
|
314
|
+
ContactEducationHistoryEntity: actionParams.contactEducationHistoryEntityFields || null
|
|
116
315
|
};
|
|
117
|
-
const task = this.ProcessSingleDomain(processParams, index, resultLength, md,
|
|
316
|
+
const task = this.ProcessSingleDomain(processParams, index, resultLength, md, contextUser);
|
|
118
317
|
tasks.push(task);
|
|
119
318
|
if (tasks.length === Config.ConcurrentGroups || index + 1 >= resultLength) {
|
|
120
|
-
// if we started n tasks or reached the end of the result array, await all started tasks
|
|
121
319
|
await Promise.all(tasks);
|
|
122
|
-
// clear tasks
|
|
123
320
|
tasks.length = 0;
|
|
124
321
|
}
|
|
125
322
|
}
|
|
126
|
-
if (resultLength > 0) {
|
|
127
|
-
// restart the process once more to see if any records remaining
|
|
128
|
-
(0, core_1.LogStatus)('--- Records left, RESTARTING PROCESS...');
|
|
129
|
-
return await this.InternalRunAction(params);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
(0, core_1.LogStatus)('+++ Completed domain enrichment, no records left to process.');
|
|
133
|
-
return {
|
|
134
|
-
Success: true,
|
|
135
|
-
Message: "ApolloAccountsEnrichment executed successfully.",
|
|
136
|
-
ResultCode: "SUCCESS"
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Processes a single domain for both organization and people enrichment
|
|
326
|
+
* This is the main orchestration method for enriching one account
|
|
327
|
+
*
|
|
328
|
+
* @param params - Domain processing parameters with entity configurations
|
|
329
|
+
* @param startRow - Current row number for progress tracking
|
|
330
|
+
* @param totalRows - Total number of rows for progress tracking
|
|
331
|
+
* @param md - Metadata instance for entity operations
|
|
332
|
+
* @param currentUser - User context for data access
|
|
333
|
+
* @returns Promise<boolean> indicating success/failure of domain processing
|
|
334
|
+
*/
|
|
140
335
|
async ProcessSingleDomain(params, startRow, totalRows, md, currentUser) {
|
|
141
336
|
const record = params.Record;
|
|
142
|
-
const accountID = record[params.AccountEntity.
|
|
143
|
-
const domain = record[params.AccountEntity.
|
|
144
|
-
const enrichOrganizationResult = await this.
|
|
145
|
-
const createAndEnrichPeopleResult = await this.
|
|
337
|
+
const accountID = record[params.AccountEntity.AccountIDField];
|
|
338
|
+
const domain = record[params.AccountEntity.DomainField];
|
|
339
|
+
const enrichOrganizationResult = await this.enrichOrganization(params, startRow, totalRows, md, currentUser);
|
|
340
|
+
const createAndEnrichPeopleResult = await this.createAndEnrichPeople(params, accountID, domain, startRow, md, currentUser);
|
|
146
341
|
return enrichOrganizationResult && createAndEnrichPeopleResult;
|
|
147
342
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
if (params.AccountEntity.CityFieldNameName) {
|
|
176
|
-
accountEntity.Set(params.AccountEntity.CityFieldNameName, organization.city);
|
|
177
|
-
}
|
|
178
|
-
if (params.AccountEntity.CityFieldNameName) {
|
|
179
|
-
accountEntity.Set(params.AccountEntity.CityFieldNameName, organization.city);
|
|
180
|
-
}
|
|
181
|
-
if (params.AccountEntity.StateProvinceFieldName) {
|
|
182
|
-
//accountEntity.Set(params.StateProvinceFieldName, organization.state);
|
|
183
|
-
}
|
|
184
|
-
if (params.AccountEntity.PostalCodeFieldName) {
|
|
185
|
-
accountEntity.Set(params.AccountEntity.PostalCodeFieldName, organization.postal_code);
|
|
186
|
-
}
|
|
187
|
-
if (params.AccountEntity.DescriptionFieldName) {
|
|
188
|
-
accountEntity.Set(params.AccountEntity.DescriptionFieldName, organization.short_description);
|
|
189
|
-
}
|
|
190
|
-
if (params.AccountEntity.PhoneNumberFieldName) {
|
|
191
|
-
accountEntity.Set(params.AccountEntity.PhoneNumberFieldName, organization.phone);
|
|
192
|
-
}
|
|
193
|
-
if (params.AccountEntity.CountryFieldName) {
|
|
194
|
-
accountEntity.Set(params.AccountEntity.CountryFieldName, organization.country);
|
|
195
|
-
}
|
|
196
|
-
if (params.AccountEntity.LinkedInFieldName) {
|
|
197
|
-
accountEntity.Set(params.AccountEntity.LinkedInFieldName, organization.linkedin_url);
|
|
198
|
-
}
|
|
199
|
-
if (params.AccountEntity.LogoURLFieldName) {
|
|
200
|
-
accountEntity.Set(params.AccountEntity.LogoURLFieldName, organization.logo_url);
|
|
201
|
-
}
|
|
202
|
-
if (params.AccountEntity.FacebookFieldName) {
|
|
203
|
-
accountEntity.Set(params.AccountEntity.FacebookFieldName, organization.facebook_url);
|
|
343
|
+
/**
|
|
344
|
+
* Enriches organization data using Apollo.io's organization enrichment endpoint
|
|
345
|
+
* Updates account record with company information and triggers technology processing
|
|
346
|
+
*
|
|
347
|
+
* @param params - Domain processing parameters
|
|
348
|
+
* @param startRow - Current row for progress tracking
|
|
349
|
+
* @param totalRows - Total rows for progress tracking
|
|
350
|
+
* @param md - Metadata instance for entity operations
|
|
351
|
+
* @param currentUser - User context for data access
|
|
352
|
+
* @returns Promise<boolean> indicating success/failure
|
|
353
|
+
*/
|
|
354
|
+
async enrichOrganization(params, startRow, totalRows, md, currentUser) {
|
|
355
|
+
try {
|
|
356
|
+
const record = params.Record;
|
|
357
|
+
const orgQueryString = {
|
|
358
|
+
api_key: Config.ApolloAPIKey,
|
|
359
|
+
domain: record[params.AccountEntity.DomainField]
|
|
360
|
+
};
|
|
361
|
+
const headers = {
|
|
362
|
+
'Cache-Control': 'no-cache',
|
|
363
|
+
'Content-Type': 'application/json'
|
|
364
|
+
};
|
|
365
|
+
// Get the organization enrichment data
|
|
366
|
+
const orgResponse = await this.WrapApolloCall('get', 'organizations/enrich', null, { params: orgQueryString, headers: headers });
|
|
367
|
+
if (!orgResponse || orgResponse.status !== 200) {
|
|
368
|
+
return false;
|
|
204
369
|
}
|
|
205
|
-
|
|
206
|
-
|
|
370
|
+
const result = orgResponse.data;
|
|
371
|
+
let bSuccess = true;
|
|
372
|
+
if (result && result.organization) {
|
|
373
|
+
bSuccess = await this.updateAccountWithOrganizationData(result.organization, params, record, md, currentUser);
|
|
374
|
+
if (params.AccountTechnologyEntity && bSuccess) {
|
|
375
|
+
const accountID = record[params.AccountEntity.AccountIDField];
|
|
376
|
+
bSuccess = bSuccess && await this.CreateAccountTechnologyRecords(params, accountID, result.organization.current_technologies, md, currentUser);
|
|
377
|
+
}
|
|
207
378
|
}
|
|
208
|
-
|
|
209
|
-
|
|
379
|
+
else {
|
|
380
|
+
// No match found, but still update enriched timestamp
|
|
381
|
+
(0, core_1.LogStatus)(` > No matches found for this domain: ${params.Record[params.AccountEntity.DomainField]}`);
|
|
382
|
+
bSuccess = await this.updateAccountEnrichedTimestamp(params, record, md, currentUser);
|
|
210
383
|
}
|
|
211
|
-
|
|
212
|
-
|
|
384
|
+
return bSuccess;
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
(0, core_1.LogError)('Error enriching organization', undefined, error);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Updates an account entity with enriched organization data from Apollo.io
|
|
393
|
+
* Maps Apollo organization fields to the configured account entity fields
|
|
394
|
+
*
|
|
395
|
+
* @param organization - Apollo organization data
|
|
396
|
+
* @param params - Processing parameters with field mappings
|
|
397
|
+
* @param record - Current account record data
|
|
398
|
+
* @param md - Metadata instance for entity operations
|
|
399
|
+
* @param currentUser - User context for data access
|
|
400
|
+
* @returns Promise<boolean> indicating save success/failure
|
|
401
|
+
*/
|
|
402
|
+
async updateAccountWithOrganizationData(organization, params, record, md, currentUser) {
|
|
403
|
+
try {
|
|
404
|
+
const accountEntity = await md.GetEntityObject(params.AccountEntity.EntityName, core_1.CompositeKey.FromID(record.ID), currentUser);
|
|
405
|
+
// Set organization data fields
|
|
406
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.AddressField, organization.street_address);
|
|
407
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.CityField, organization.city);
|
|
408
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.StateProvinceField, organization.state);
|
|
409
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.PostalCodeField, organization.postal_code);
|
|
410
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.DescriptionField, organization.short_description);
|
|
411
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.PhoneNumberField, organization.phone);
|
|
412
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.CountryField, organization.country);
|
|
413
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.LinkedInField, organization.linkedin_url);
|
|
414
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.LogoURLField, organization.logo_url);
|
|
415
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.FacebookField, organization.facebook_url);
|
|
416
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.TwitterField, organization.twitter_url);
|
|
417
|
+
this.setFieldIfExists(accountEntity, params.AccountEntity.EnrichedAtField, new Date());
|
|
213
418
|
const saveResult = await accountEntity.Save();
|
|
214
419
|
if (!saveResult) {
|
|
215
420
|
(0, core_1.LogError)('Error updating account record with enriched data', undefined, accountEntity.LatestResult);
|
|
216
421
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return bSuccess;
|
|
422
|
+
return saveResult;
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
(0, core_1.LogError)('Error updating account with organization data', undefined, error);
|
|
426
|
+
return false;
|
|
223
427
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Updates the enrichment timestamp on an account even when no data was found
|
|
431
|
+
* Prevents repeated attempts to enrich accounts with no Apollo data
|
|
432
|
+
*
|
|
433
|
+
* @param params - Processing parameters with field mappings
|
|
434
|
+
* @param record - Current account record data
|
|
435
|
+
* @param md - Metadata instance for entity operations
|
|
436
|
+
* @param currentUser - User context for data access
|
|
437
|
+
* @returns Promise<boolean> indicating save success/failure
|
|
438
|
+
*/
|
|
439
|
+
async updateAccountEnrichedTimestamp(params, record, md, currentUser) {
|
|
440
|
+
try {
|
|
441
|
+
const accountEntity = await md.GetEntityObject('Accounts', core_1.CompositeKey.FromID(record.ID), currentUser);
|
|
231
442
|
accountEntity.Set(params.AccountEntity.EnrichedAtField, new Date());
|
|
232
443
|
const saveResult = await accountEntity.Save();
|
|
233
444
|
if (!saveResult) {
|
|
234
|
-
(0, core_1.LogError)('Error updating account record with enriched
|
|
445
|
+
(0, core_1.LogError)('Error updating account record with enriched timestamp', undefined, accountEntity.LatestResult);
|
|
235
446
|
}
|
|
236
447
|
return saveResult;
|
|
237
448
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
let bSuccess = true;
|
|
241
|
-
const ATEntity = params.AccountTechnologyEntity;
|
|
242
|
-
if (!ATEntity) {
|
|
449
|
+
catch (error) {
|
|
450
|
+
(0, core_1.LogError)('Error updating account enriched timestamp', undefined, error);
|
|
243
451
|
return false;
|
|
244
452
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Safely sets a field on an entity if the field name is defined and value is not null
|
|
456
|
+
* Prevents errors from undefined field mappings
|
|
457
|
+
*
|
|
458
|
+
* @param entity - Target entity to update
|
|
459
|
+
* @param fieldName - Name of the field (can be undefined)
|
|
460
|
+
* @param value - Value to set (checked for null/undefined)
|
|
461
|
+
*/
|
|
462
|
+
setFieldIfExists(entity, fieldName, value) {
|
|
463
|
+
if (fieldName && value != null) {
|
|
464
|
+
entity.Set(fieldName, value);
|
|
253
465
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Creates or updates technology records associated with an account
|
|
469
|
+
* Manages the relationship between accounts and their technology stack
|
|
470
|
+
* Marks old technologies as ended if no longer detected
|
|
471
|
+
*
|
|
472
|
+
* @param params - Processing parameters with technology entity configuration
|
|
473
|
+
* @param accountID - ID of the account to associate technologies with
|
|
474
|
+
* @param technologies - Array of technology data from Apollo
|
|
475
|
+
* @param md - Metadata instance for entity operations
|
|
476
|
+
* @param currentUser - User context for data access
|
|
477
|
+
* @returns Promise<boolean> indicating overall success/failure
|
|
478
|
+
*/
|
|
479
|
+
async CreateAccountTechnologyRecords(params, accountID, technologies, md, currentUser) {
|
|
480
|
+
if (!params.AccountTechnologyEntity || !technologies || technologies.length === 0) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
let bSuccess = true;
|
|
485
|
+
const ATEntity = params.AccountTechnologyEntity;
|
|
486
|
+
if (ATEntity.TechnologyIDField && ATEntity.TechnologyField && ATEntity.CategoryField) {
|
|
487
|
+
const rv = new core_1.RunView();
|
|
488
|
+
const runViewResult = await rv.RunView({
|
|
489
|
+
EntityName: ATEntity.EntityName,
|
|
490
|
+
ExtraFilter: `${ATEntity.AccountIDField} = ${accountID}`
|
|
491
|
+
}, currentUser);
|
|
492
|
+
if (!runViewResult.Success) {
|
|
493
|
+
(0, core_1.LogError)('Error querying for existing Account Technologies', undefined, runViewResult.ErrorMessage);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
// Process each technology
|
|
497
|
+
for (const technology of technologies) {
|
|
498
|
+
const categoryId = await this.getOrCreateTechnologyCategoryId(params, technology.category, md, currentUser);
|
|
499
|
+
const existingRow = runViewResult.Results.find((r) => r[ATEntity.CategoryField]?.trim().toLowerCase() === technology.category.trim().toLowerCase() &&
|
|
500
|
+
r[ATEntity.TechnologyField]?.trim().toLowerCase() === technology.name.trim().toLowerCase());
|
|
501
|
+
if (!existingRow) {
|
|
502
|
+
// Create new technology record
|
|
503
|
+
const at = await md.GetEntityObject(ATEntity.EntityName, currentUser);
|
|
504
|
+
at.NewRecord();
|
|
505
|
+
at.Set(ATEntity.AccountIDField, accountID);
|
|
506
|
+
at.Set(ATEntity.TechnologyIDField, await this.getOrCreateTechnologyId(technology.name, categoryId, md, currentUser));
|
|
507
|
+
bSuccess = bSuccess && await at.Save();
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
existingRow.matchFound = true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Mark unmatched technologies as ended
|
|
514
|
+
for (const record of runViewResult.Results) {
|
|
515
|
+
if (!record.matchFound) {
|
|
516
|
+
const entity = await md.GetEntityObject(ATEntity.EntityName, core_1.CompositeKey.FromID(record.ID), currentUser);
|
|
517
|
+
entity.Set(ATEntity.EndedUseAtField, new Date());
|
|
518
|
+
const saveResult = await entity.Save();
|
|
519
|
+
if (!saveResult) {
|
|
520
|
+
(0, core_1.LogError)('Error updating Account Technology record', undefined, entity.LatestResult);
|
|
521
|
+
}
|
|
522
|
+
bSuccess = bSuccess && saveResult;
|
|
523
|
+
}
|
|
278
524
|
}
|
|
279
|
-
bSuccess = bSuccess && saveResult;
|
|
280
525
|
}
|
|
526
|
+
return bSuccess;
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
(0, core_1.LogError)('Error creating account technology records', undefined, error);
|
|
530
|
+
return false;
|
|
281
531
|
}
|
|
282
|
-
return bSuccess;
|
|
283
532
|
}
|
|
284
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Gets or creates a technology category record by name
|
|
535
|
+
* Ensures technology categories exist before creating technology records
|
|
536
|
+
*
|
|
537
|
+
* @param params - Processing parameters with category entity configuration
|
|
538
|
+
* @param categoryName - Name of the technology category
|
|
539
|
+
* @param md - Metadata instance for entity operations
|
|
540
|
+
* @param currentUser - User context for data access
|
|
541
|
+
* @returns Promise<number> - ID of the category (0 if error)
|
|
542
|
+
*/
|
|
543
|
+
async getOrCreateTechnologyCategoryId(params, categoryName, md, currentUser) {
|
|
285
544
|
const TCEntity = params.TechnologyCategoryEntity;
|
|
286
545
|
if (!TCEntity) {
|
|
287
546
|
return 0;
|
|
288
547
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
548
|
+
try {
|
|
549
|
+
const rv = new core_1.RunView();
|
|
550
|
+
const runViewResult = await rv.RunView({
|
|
551
|
+
EntityName: TCEntity.EntityName,
|
|
552
|
+
ExtraFilter: `${TCEntity.NameField} = '${this.EscapeSingleQuotes(categoryName)}'`
|
|
553
|
+
}, currentUser);
|
|
554
|
+
if (!runViewResult.Success) {
|
|
555
|
+
(0, core_1.LogError)('Error querying for existing Technology Categories', undefined, runViewResult.ErrorMessage);
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
if (runViewResult.Results.length > 0) {
|
|
559
|
+
return runViewResult.Results[0].ID;
|
|
560
|
+
}
|
|
561
|
+
// Create new record
|
|
562
|
+
const entity = await md.GetEntityObject(TCEntity.EntityName, currentUser);
|
|
563
|
+
entity.NewRecord();
|
|
564
|
+
entity.Set(TCEntity.NameField, categoryName);
|
|
565
|
+
const saveResult = await entity.Save();
|
|
566
|
+
if (!saveResult) {
|
|
567
|
+
(0, core_1.LogError)('Error creating new Technology Category record', undefined, entity.LatestResult);
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
return entity.Get(TCEntity.IDField);
|
|
300
571
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
entity.Set(TCEntity.NameFieldName, categoryName);
|
|
304
|
-
const saveResult = await entity.Save();
|
|
305
|
-
if (!saveResult) {
|
|
306
|
-
(0, core_1.LogError)('Error creating new Technology Category record', undefined, entity.LatestResult);
|
|
572
|
+
catch (error) {
|
|
573
|
+
(0, core_1.LogError)('Error getting or creating technology category', undefined, error);
|
|
307
574
|
return 0;
|
|
308
575
|
}
|
|
309
|
-
return entity.Get(TCEntity.IDFieldName);
|
|
310
576
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Gets or creates a technology record by name and category
|
|
579
|
+
* Maintains referential integrity between technologies and categories
|
|
580
|
+
*
|
|
581
|
+
* @param name - Name of the technology
|
|
582
|
+
* @param categoryId - ID of the technology category
|
|
583
|
+
* @param md - Metadata instance for entity operations
|
|
584
|
+
* @param currentUser - User context for data access
|
|
585
|
+
* @returns Promise<number> - ID of the technology (0 if error)
|
|
586
|
+
*/
|
|
587
|
+
async getOrCreateTechnologyId(name, categoryId, md, currentUser) {
|
|
588
|
+
try {
|
|
589
|
+
const rv = new core_1.RunView();
|
|
590
|
+
const runViewResult = await rv.RunView({
|
|
591
|
+
EntityName: 'Technologies',
|
|
592
|
+
ExtraFilter: `Name = '${this.EscapeSingleQuotes(name)}' AND CategoryID = ${categoryId}`
|
|
593
|
+
}, currentUser);
|
|
594
|
+
if (!runViewResult.Success) {
|
|
595
|
+
(0, core_1.LogError)('Error querying for existing Technologies', undefined, runViewResult.ErrorMessage);
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
if (runViewResult.Results.length > 0) {
|
|
599
|
+
return runViewResult.Results[0].ID;
|
|
600
|
+
}
|
|
601
|
+
// Create new technology record
|
|
325
602
|
const technologyEntity = await md.GetEntityObject('Technologies', currentUser);
|
|
603
|
+
technologyEntity.NewRecord();
|
|
326
604
|
technologyEntity.Set('Name', name);
|
|
327
605
|
technologyEntity.Set('CategoryID', categoryId);
|
|
328
606
|
const saveResult = await technologyEntity.Save();
|
|
@@ -332,212 +610,342 @@ let ApolloAccountsEnrichmentAction = class ApolloAccountsEnrichmentAction extend
|
|
|
332
610
|
}
|
|
333
611
|
return technologyEntity.Get('ID');
|
|
334
612
|
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
(0, core_1.LogError)('Error getting or creating technology', undefined, error);
|
|
615
|
+
return 0;
|
|
616
|
+
}
|
|
335
617
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Searches for and enriches people associated with an organization's domain
|
|
620
|
+
* Uses Apollo's people search to find contacts at the organization
|
|
621
|
+
* Handles pagination to retrieve all available contacts (up to configured limit)
|
|
622
|
+
*
|
|
623
|
+
* @param params - Processing parameters with contact entity configuration
|
|
624
|
+
* @param accountID - ID of the account to associate contacts with
|
|
625
|
+
* @param domain - Organization domain to search for people
|
|
626
|
+
* @param startRow - Current row for progress tracking
|
|
627
|
+
* @param md - Metadata instance for entity operations
|
|
628
|
+
* @param currentUser - User context for data access
|
|
629
|
+
* @returns Promise<boolean> indicating success/failure of people processing
|
|
630
|
+
*/
|
|
631
|
+
async createAndEnrichPeople(params, accountID, domain, startRow, md, currentUser) {
|
|
632
|
+
if (!params.ContactEntity) {
|
|
633
|
+
return true; // No contact processing needed
|
|
351
634
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
635
|
+
try {
|
|
636
|
+
const APIparams = {
|
|
637
|
+
api_key: Config.ApolloAPIKey,
|
|
638
|
+
reveal_personal_emails: true,
|
|
639
|
+
q_organization_domains: domain,
|
|
640
|
+
page: 1
|
|
641
|
+
};
|
|
642
|
+
const headers = {
|
|
643
|
+
'Cache-Control': 'no-cache',
|
|
644
|
+
'Content-Type': 'application/json'
|
|
645
|
+
};
|
|
646
|
+
const response = await this.WrapApolloCall('post', 'mixed_people/search', APIparams, { headers: headers });
|
|
647
|
+
if (!response || response.status !== 200) {
|
|
648
|
+
(0, core_1.LogError)(`Error fetching data from Apollo.io: ${response?.statusText}`);
|
|
362
649
|
return false;
|
|
363
650
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
651
|
+
const data = response.data;
|
|
652
|
+
const allPeople = data.people;
|
|
653
|
+
// Fetch additional pages
|
|
654
|
+
for (let i = 1; i < data.pagination.total_pages && i < (Config.MaxPeopleToEnrichPerOrg / 10); i++) {
|
|
655
|
+
APIparams.page = i + 1;
|
|
656
|
+
const nextResponse = await this.WrapApolloCall('post', 'mixed_people/search', APIparams, { headers: headers });
|
|
657
|
+
if (nextResponse.status !== 200) {
|
|
658
|
+
console.error(`Error fetching data from Apollo.io: ${nextResponse.statusText}`);
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
367
661
|
const nextResult = nextResponse.data;
|
|
368
662
|
allPeople.push(...nextResult.people);
|
|
369
663
|
}
|
|
664
|
+
if (allPeople.length === 0) {
|
|
665
|
+
(0, core_1.LogStatus)(`No people found for this domain: ${domain}`);
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
// Process all people
|
|
669
|
+
let bSuccess = true;
|
|
670
|
+
for (let i = 0; i < allPeople.length; i++) {
|
|
671
|
+
const person = allPeople[i];
|
|
672
|
+
if (this.shouldSkipPerson(person, domain)) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
bSuccess = bSuccess && await this.createAndEnrichContact(params, accountID, person, md, currentUser, i, allPeople.length);
|
|
676
|
+
}
|
|
677
|
+
return bSuccess;
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
(0, core_1.LogError)('Error creating and enriching people', undefined, error);
|
|
681
|
+
return false;
|
|
370
682
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Determines if a person should be skipped during contact creation
|
|
686
|
+
* Filters out people with missing data or excluded job titles
|
|
687
|
+
*
|
|
688
|
+
* @param person - Person data from Apollo search response
|
|
689
|
+
* @param domain - Expected domain for email validation
|
|
690
|
+
* @returns boolean - true if person should be skipped
|
|
691
|
+
*/
|
|
692
|
+
shouldSkipPerson(person, domain) {
|
|
693
|
+
// Check for missing required data
|
|
694
|
+
if (!person.last_name || !person.email || person.email.split('@')[1] !== domain) {
|
|
695
|
+
(0, core_1.LogStatus)(`Skipping person: ${person.first_name} ${person.last_name} because email is missing or does not match domain`);
|
|
374
696
|
return true;
|
|
375
697
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (!person.last_name || !person.email || person.email.split('@')[1] !== domain) {
|
|
381
|
-
// skip this person
|
|
382
|
-
(0, core_1.LogStatus)(`Skipping person: ${person.first_name} ${person.last_name}, accountID: ${accountID} because email is missing or does not match domain`);
|
|
383
|
-
}
|
|
384
|
-
else if (this.IsExcludedTitle(person.title)) {
|
|
385
|
-
(0, core_1.LogStatus)(`Skipping person: ${person.first_name} ${person.last_name}, accountID: ${accountID} because title is excluded`);
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
bSuccess = bSuccess && await this.CreateAndEnrichContact(params, accountID, person, md, currentUser, i, allPeople.length);
|
|
389
|
-
}
|
|
390
|
-
i++;
|
|
698
|
+
// Check for excluded titles
|
|
699
|
+
if (this.IsExcludedTitle(person.title)) {
|
|
700
|
+
(0, core_1.LogStatus)(`Skipping person: ${person.first_name} ${person.last_name} because title is excluded`);
|
|
701
|
+
return true;
|
|
391
702
|
}
|
|
392
|
-
return
|
|
703
|
+
return false;
|
|
393
704
|
}
|
|
394
|
-
|
|
705
|
+
/**
|
|
706
|
+
* Creates or updates a contact record with enriched data from Apollo
|
|
707
|
+
* Handles contact deduplication and data validation
|
|
708
|
+
*
|
|
709
|
+
* @param params - Processing parameters with contact entity configuration
|
|
710
|
+
* @param accountID - ID of the account to associate contact with
|
|
711
|
+
* @param person - Person data from Apollo search response
|
|
712
|
+
* @param md - Metadata instance for entity operations
|
|
713
|
+
* @param currentUser - User context for data access
|
|
714
|
+
* @param sequence - Current contact number for progress tracking
|
|
715
|
+
* @param total - Total contacts for progress tracking
|
|
716
|
+
* @returns Promise<boolean> indicating success/failure
|
|
717
|
+
*/
|
|
718
|
+
async createAndEnrichContact(params, accountID, person, md, currentUser, sequence, total) {
|
|
395
719
|
const CEntity = params.ContactEntity;
|
|
396
720
|
if (!CEntity) {
|
|
397
721
|
return false;
|
|
398
722
|
}
|
|
399
723
|
try {
|
|
400
724
|
(0, core_1.LogStatus)(`Enriching contact for person: ${person.first_name} ${person.last_name}, accountID: ${accountID} (${sequence} of ${total})`);
|
|
401
|
-
|
|
402
|
-
const contact = await this.GetOrCreateContact(params, accountID, person, md, currentUser);
|
|
725
|
+
const contact = await this.getOrCreateContact(params, accountID, person, md, currentUser);
|
|
403
726
|
if (!contact) {
|
|
404
727
|
return false;
|
|
405
728
|
}
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
contact.
|
|
409
|
-
contact.Set(CEntity.TitleFieldName, person.title);
|
|
410
|
-
if (person.email) {
|
|
411
|
-
contact.Set(CEntity.EmailFieldName, person.email);
|
|
412
|
-
}
|
|
413
|
-
contact.Set(CEntity.FacebookFieldName, person.facebook_url);
|
|
414
|
-
contact.Set(CEntity.LinkedInFieldName, person.linkedin_url);
|
|
415
|
-
contact.Set(CEntity.TwitterFieldName, person.twitter_url);
|
|
416
|
-
//contact.Set("PhoneNumber", person.?.length > 0 ? person.phone_numbers[0].raw_number : null;
|
|
417
|
-
contact.Set(CEntity.PhotoFieldName, person.photo_url);
|
|
418
|
-
contact.Set(CEntity.LastEnrichedAtFieldName, new Date());
|
|
419
|
-
const saveResult = contact.Save();
|
|
729
|
+
// Update contact with enriched data
|
|
730
|
+
this.updateContactFields(contact, person, CEntity);
|
|
731
|
+
const saveResult = await contact.Save();
|
|
420
732
|
if (!saveResult) {
|
|
421
733
|
(0, core_1.LogError)('Error updating contact record with enriched data', undefined, contact.LatestResult);
|
|
422
734
|
return false;
|
|
423
735
|
}
|
|
424
|
-
|
|
736
|
+
// Update employment and education history if configured
|
|
737
|
+
if (params.ContactEducationHistoryEntity) {
|
|
738
|
+
return await this.createUpdateContactEmploymentAndEducationHistory(params, contact.Get("ID"), person, md, currentUser);
|
|
739
|
+
}
|
|
740
|
+
return true;
|
|
425
741
|
}
|
|
426
|
-
catch (
|
|
427
|
-
(0, core_1.LogError)(
|
|
742
|
+
catch (error) {
|
|
743
|
+
(0, core_1.LogError)('Error creating and enriching contact', undefined, error);
|
|
428
744
|
return false;
|
|
429
745
|
}
|
|
430
746
|
}
|
|
431
|
-
|
|
747
|
+
/**
|
|
748
|
+
* Updates contact entity fields with person data from Apollo
|
|
749
|
+
* Maps Apollo person fields to the configured contact entity fields
|
|
750
|
+
*
|
|
751
|
+
* @param contact - Contact entity to update
|
|
752
|
+
* @param person - Person data from Apollo
|
|
753
|
+
* @param CEntity - Contact entity field name mappings
|
|
754
|
+
*/
|
|
755
|
+
updateContactFields(contact, person, CEntity) {
|
|
756
|
+
contact.Set(CEntity.FirstNameField, person.first_name);
|
|
757
|
+
contact.Set(CEntity.LastNameField, person.last_name);
|
|
758
|
+
contact.Set(CEntity.TitleField, person.title);
|
|
759
|
+
if (person.email) {
|
|
760
|
+
contact.Set(CEntity.EmailField, person.email);
|
|
761
|
+
}
|
|
762
|
+
this.setFieldIfExists(contact, CEntity.FacebookField, person.facebook_url);
|
|
763
|
+
this.setFieldIfExists(contact, CEntity.LinkedInField, person.linkedin_url);
|
|
764
|
+
this.setFieldIfExists(contact, CEntity.TwitterField, person.twitter_url);
|
|
765
|
+
this.setFieldIfExists(contact, CEntity.ProfilePictureURLField, person.photo_url);
|
|
766
|
+
contact.Set(CEntity.EnrichedAtField, new Date());
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Gets an existing contact or creates a new one with conflict detection
|
|
770
|
+
* Prevents duplicate contacts across different accounts
|
|
771
|
+
* Supports matching by email or name combination
|
|
772
|
+
*
|
|
773
|
+
* @param params - Processing parameters with contact entity configuration
|
|
774
|
+
* @param accountID - ID of the account to associate contact with
|
|
775
|
+
* @param person - Person data from Apollo
|
|
776
|
+
* @param md - Metadata instance for entity operations
|
|
777
|
+
* @param currentUser - User context for data access
|
|
778
|
+
* @returns Promise<BaseEntity | null> - Contact entity or null if conflict/error
|
|
779
|
+
*/
|
|
780
|
+
async getOrCreateContact(params, accountID, person, md, currentUser) {
|
|
432
781
|
const CEntity = params.ContactEntity;
|
|
433
782
|
if (!CEntity) {
|
|
434
783
|
return null;
|
|
435
784
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
785
|
+
try {
|
|
786
|
+
const rv = new core_1.RunView();
|
|
787
|
+
// Check if contact exists with different account
|
|
788
|
+
const conflictResult = await rv.RunView({
|
|
789
|
+
EntityName: CEntity.EntityName,
|
|
790
|
+
ExtraFilter: `${CEntity.EmailField} = '${this.EscapeSingleQuotes(person.email)}' AND ${CEntity.AccountIDField} <> ${accountID}`
|
|
791
|
+
}, currentUser);
|
|
792
|
+
if (!conflictResult.Success) {
|
|
793
|
+
(0, core_1.LogError)('Error querying for existing Contacts', undefined, conflictResult.ErrorMessage);
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
if (conflictResult.Results.length > 0) {
|
|
797
|
+
const existing = conflictResult.Results[0];
|
|
798
|
+
(0, core_1.LogError)(`*** SKIPPING new contact record for ${person.first_name} ${person.last_name} because they are already associated with AccountID: ${existing.AccountID} - ContactID: ${existing.ID}`);
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
// Check if contact exists for this account
|
|
802
|
+
const existingResult = await rv.RunView({
|
|
803
|
+
EntityName: CEntity.EntityName,
|
|
804
|
+
ExtraFilter: `(${CEntity.EmailField} = '${this.EscapeSingleQuotes(person.email)}' OR (${CEntity.FirstNameField} = '${this.EscapeSingleQuotes(person.first_name)}' AND ${CEntity.LastNameField} = '${this.EscapeSingleQuotes(person.last_name)}')) AND ${CEntity.AccountIDField} = ${accountID}`
|
|
805
|
+
}, currentUser);
|
|
806
|
+
if (!existingResult.Success) {
|
|
807
|
+
(0, core_1.LogError)('Error querying for existing Contacts', undefined, existingResult.ErrorMessage);
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
let contactEntity = null;
|
|
811
|
+
if (existingResult.Results.length > 0) {
|
|
812
|
+
// Update existing contact
|
|
813
|
+
contactEntity = await md.GetEntityObject(CEntity.EntityName, existingResult.Results[0].ID, currentUser);
|
|
814
|
+
contactEntity.Set(CEntity.EmailField, person.email);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
// Create new contact
|
|
818
|
+
(0, core_1.LogStatus)(` > Creating new contact record for ${person.first_name} ${person.last_name}`);
|
|
819
|
+
contactEntity = await md.GetEntityObject(CEntity.EntityName, currentUser);
|
|
820
|
+
contactEntity.NewRecord();
|
|
821
|
+
contactEntity.Set(CEntity.EmailField, person.email);
|
|
822
|
+
contactEntity.Set(CEntity.AccountIDField, accountID);
|
|
823
|
+
contactEntity.Set(CEntity.ActivityCountField, 0);
|
|
824
|
+
contactEntity.Set(CEntity.EmailSourceField, Config.EmailSourceName);
|
|
825
|
+
}
|
|
826
|
+
return contactEntity;
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
(0, core_1.LogError)('Error getting or creating contact', undefined, error);
|
|
459
830
|
return null;
|
|
460
831
|
}
|
|
461
|
-
const contactEntity = await md.GetEntityObject(CEntity.EntityName, currentUser);
|
|
462
|
-
if (contactsViewResult.Results.length > 0) {
|
|
463
|
-
contactEntity.SetMany(contactsViewResult.Results[0]);
|
|
464
|
-
contactEntity.Set(CEntity.EmailFieldName, person.email);
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
(0, core_1.LogStatus)(` > Creating new contact record for ${person.first_name} ${person.last_name}`);
|
|
468
|
-
contactEntity.NewRecord();
|
|
469
|
-
contactEntity.Set(CEntity.EmailFieldName, person.email);
|
|
470
|
-
contactEntity.Set(CEntity.AccountIDFieldName, accountID);
|
|
471
|
-
contactEntity.Set(CEntity.ActivityCountFieldName, 0);
|
|
472
|
-
contactEntity.Set(CEntity.EmailSourceFieldName, Config.EmailSourceName);
|
|
473
|
-
}
|
|
474
|
-
return contactEntity;
|
|
475
832
|
}
|
|
476
|
-
|
|
833
|
+
/**
|
|
834
|
+
* Creates or updates employment and education history records for a contact
|
|
835
|
+
* Processes Apollo employment history data to create education records
|
|
836
|
+
*
|
|
837
|
+
* @param params - Processing parameters with education entity configuration
|
|
838
|
+
* @param contactID - ID of the contact to create history for
|
|
839
|
+
* @param person - Person data containing employment history
|
|
840
|
+
* @param md - Metadata instance for entity operations
|
|
841
|
+
* @param currentUser - User context for data access
|
|
842
|
+
* @returns Promise<boolean> indicating success/failure
|
|
843
|
+
*/
|
|
844
|
+
async createUpdateContactEmploymentAndEducationHistory(params, contactID, person, md, currentUser) {
|
|
477
845
|
const CEntity = params.ContactEducationHistoryEntity;
|
|
478
|
-
if (!CEntity) {
|
|
479
|
-
return
|
|
846
|
+
if (!CEntity || !person.employment_history) {
|
|
847
|
+
return true;
|
|
480
848
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
historyEntity
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
historyEntity.Set(CEntity.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
849
|
+
try {
|
|
850
|
+
let bSuccess = true;
|
|
851
|
+
const rv = new core_1.RunView();
|
|
852
|
+
for (const history of person.employment_history) {
|
|
853
|
+
if (history.degree && history.degree.length > 0) {
|
|
854
|
+
const EHRunViewResult = await rv.RunView({
|
|
855
|
+
EntityName: CEntity.EntityName,
|
|
856
|
+
ExtraFilter: `${CEntity.ContactIDField} = ${contactID} AND ${CEntity.InstitutionField} = '${this.EscapeSingleQuotes(history.organization_name)}' AND ${CEntity.DegreeField} = '${this.EscapeSingleQuotes(history.degree)}'`
|
|
857
|
+
}, currentUser);
|
|
858
|
+
if (!EHRunViewResult.Success) {
|
|
859
|
+
(0, core_1.LogError)('Error querying for existing Employment History', undefined, EHRunViewResult.ErrorMessage);
|
|
860
|
+
bSuccess = false;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
let historyEntity = null;
|
|
864
|
+
if (EHRunViewResult.Results.length > 0) {
|
|
865
|
+
historyEntity = await md.GetEntityObject(CEntity.EntityName, core_1.CompositeKey.FromID(EHRunViewResult.Results[0].ID), currentUser);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
historyEntity = await md.GetEntityObject(CEntity.EntityName, currentUser);
|
|
869
|
+
historyEntity.NewRecord();
|
|
870
|
+
historyEntity.Set(CEntity.ContactIDField, contactID);
|
|
871
|
+
}
|
|
872
|
+
// Set history data
|
|
873
|
+
historyEntity.Set(CEntity.InstitutionField, history.organization_name);
|
|
874
|
+
historyEntity.Set(CEntity.DegreeField, history.degree);
|
|
875
|
+
historyEntity.Set(CEntity.GradeLevelField, !isNaN(history.grade_level) ? parseInt(history.grade_level) : 0);
|
|
876
|
+
if (this.IsValidDate(history.start_date)) {
|
|
877
|
+
historyEntity.Set(CEntity.StartDateField, new Date(history.start_date));
|
|
878
|
+
}
|
|
879
|
+
if (this.IsValidDate(history.end_date)) {
|
|
880
|
+
historyEntity.Set(CEntity.EndDateField, new Date(history.end_date));
|
|
881
|
+
}
|
|
882
|
+
historyEntity.Set(CEntity.IsCurrentField, history.current);
|
|
883
|
+
const saveResult = await historyEntity.Save();
|
|
884
|
+
if (!saveResult) {
|
|
885
|
+
(0, core_1.LogError)('Error saving Employment History record', undefined, historyEntity.LatestResult);
|
|
886
|
+
bSuccess = false;
|
|
887
|
+
}
|
|
515
888
|
}
|
|
516
889
|
}
|
|
890
|
+
return bSuccess;
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
(0, core_1.LogError)('Error creating/updating contact employment and education history', undefined, error);
|
|
894
|
+
return false;
|
|
517
895
|
}
|
|
518
|
-
return bSuccess;
|
|
519
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* Validates if a date string represents a valid date
|
|
899
|
+
* Used for employment/education history date validation
|
|
900
|
+
*
|
|
901
|
+
* @param dateString - Date string to validate
|
|
902
|
+
* @returns boolean - true if valid date
|
|
903
|
+
*/
|
|
520
904
|
IsValidDate(dateString) {
|
|
521
905
|
if (dateString && dateString.length > 0) {
|
|
522
906
|
const date = new Date(dateString);
|
|
523
907
|
return date instanceof Date && !isNaN(date.getTime());
|
|
524
908
|
}
|
|
525
|
-
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
909
|
+
return false;
|
|
528
910
|
}
|
|
911
|
+
/**
|
|
912
|
+
* Checks if a job title should be excluded from contact creation
|
|
913
|
+
* Filters out non-business contacts like students and volunteers
|
|
914
|
+
*
|
|
915
|
+
* @param title - Job title to check
|
|
916
|
+
* @returns boolean - true if title should be excluded
|
|
917
|
+
*/
|
|
529
918
|
IsExcludedTitle(title) {
|
|
530
919
|
if (!title) {
|
|
531
920
|
return false;
|
|
532
921
|
}
|
|
533
922
|
return this.ExcludeTitles.includes(title.toLowerCase());
|
|
534
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Escapes single quotes in strings for SQL query safety
|
|
926
|
+
* Prevents SQL injection in dynamic filter construction
|
|
927
|
+
*
|
|
928
|
+
* @param str - String to escape
|
|
929
|
+
* @returns string - Escaped string with doubled single quotes
|
|
930
|
+
*/
|
|
535
931
|
EscapeSingleQuotes(str) {
|
|
536
932
|
if (str) {
|
|
537
933
|
return str.replace(/'/g, "''");
|
|
538
934
|
}
|
|
539
935
|
return '';
|
|
540
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Wraps Apollo.io API calls with retry logic for rate limiting
|
|
939
|
+
* Handles 429 (Too Many Requests) responses with exponential backoff
|
|
940
|
+
*
|
|
941
|
+
* @param method - HTTP method ('get' or 'post')
|
|
942
|
+
* @param endpoint - Apollo API endpoint path
|
|
943
|
+
* @param data - Request payload for POST requests
|
|
944
|
+
* @param config - Axios configuration object
|
|
945
|
+
* @param retryAttempts - Number of retry attempts remaining (default: 1)
|
|
946
|
+
* @returns Promise<AxiosResponse> - Apollo API response
|
|
947
|
+
* @throws Error if rate limit exceeded or other API errors
|
|
948
|
+
*/
|
|
541
949
|
async WrapApolloCall(method, endpoint, data, config, retryAttempts = 1) {
|
|
542
950
|
try {
|
|
543
951
|
const url = `${Config.ApolloAPIEndpoint}/${endpoint}`;
|
|
@@ -571,15 +979,27 @@ let ApolloAccountsEnrichmentAction = class ApolloAccountsEnrichmentAction extend
|
|
|
571
979
|
}
|
|
572
980
|
}
|
|
573
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* Utility method for creating delays in async operations
|
|
984
|
+
* Used for rate limit backoff and API throttling
|
|
985
|
+
*
|
|
986
|
+
* @param ms - Number of milliseconds to wait
|
|
987
|
+
* @returns Promise that resolves after the specified delay
|
|
988
|
+
*/
|
|
574
989
|
async Timeout(ms) {
|
|
575
990
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
576
991
|
}
|
|
577
992
|
};
|
|
578
|
-
exports.
|
|
579
|
-
exports.
|
|
580
|
-
(0, global_1.RegisterClass)(actions_1.BaseAction, "
|
|
581
|
-
],
|
|
993
|
+
exports.ApolloEnrichmentAccountsAction = ApolloEnrichmentAccountsAction;
|
|
994
|
+
exports.ApolloEnrichmentAccountsAction = ApolloEnrichmentAccountsAction = __decorate([
|
|
995
|
+
(0, global_1.RegisterClass)(actions_1.BaseAction, "ApolloEnrichmentAccountsAction")
|
|
996
|
+
], ApolloEnrichmentAccountsAction);
|
|
997
|
+
/**
|
|
998
|
+
* Loader function to ensure the ApolloEnrichmentAccountsAction class is included in the bundle
|
|
999
|
+
* Prevents tree shaking from removing this action class
|
|
1000
|
+
*/
|
|
582
1001
|
function LoadApolloAccountsEnrichmentAction() {
|
|
1002
|
+
// Stub function to prevent tree shaking
|
|
583
1003
|
}
|
|
584
1004
|
exports.LoadApolloAccountsEnrichmentAction = LoadApolloAccountsEnrichmentAction;
|
|
585
1005
|
//# sourceMappingURL=accounts.js.map
|