abmp-npm 10.3.7 → 10.3.9
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/backend/__tests__/login-email-sync.test.js +49 -0
- package/backend/__tests__/transient-retry-and-bulk-lookup.test.js +129 -0
- package/backend/consts.js +10 -0
- package/backend/contacts-methods.js +3 -0
- package/backend/daily-pull/bulk-process-methods.js +51 -11
- package/backend/daily-pull/process-member-methods.js +15 -3
- package/backend/daily-pull/utils.js +41 -3
- package/backend/http-functions/interests.js +4 -6
- package/backend/jobs.js +15 -0
- package/backend/login/generate-member-session-token.js +27 -0
- package/backend/login/index.js +4 -3
- package/backend/login/qa-login-methods.js +4 -3
- package/backend/login/sso-methods.js +11 -10
- package/backend/member-contact-orchestration.js +6 -1
- package/backend/members-area-methods.js +33 -34
- package/backend/members-data-methods.js +152 -13
- package/backend/pac-api-methods.js +12 -12
- package/backend/tasks/consts.js +2 -0
- package/backend/tasks/email-normalize-methods.js +134 -0
- package/backend/tasks/tasks-configs.js +18 -0
- package/backend/tasks/tasks-helpers-methods.js +24 -20
- package/backend/tasks/tasks-process-methods.js +33 -17
- package/backend/utils.js +42 -0
- package/package.json +4 -3
- package/pages/Home.js +0 -19
- package/public/Utils/homePage.js +1 -30
- package/public/Utils/sharedUtils.js +24 -0
- package/backend/login/login-methods-factory.js +0 -24
- package/public/Utils/homePageLoadTrace.js +0 -58
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
const { auth } = require('@wix/essentials');
|
|
2
2
|
const { members, authentication } = require('@wix/members');
|
|
3
|
+
|
|
4
|
+
const { LOGIN_EMAIL_SYNC_STATUS } = require('./consts');
|
|
5
|
+
|
|
3
6
|
const elevatedCreateMember = auth.elevate(members.createMember);
|
|
4
7
|
const elevatedChangeLoginEmail = auth.elevate(authentication.changeLoginEmail);
|
|
5
8
|
|
|
9
|
+
const LOG = '[loginEmailSync]';
|
|
10
|
+
|
|
6
11
|
function prepareMemberData(partner) {
|
|
7
12
|
const options = {
|
|
8
13
|
member: {
|
|
@@ -37,48 +42,42 @@ const getCurrentMember = async () => {
|
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
45
|
+
* Attempts to change a Wix member's login email to `member.email` and reports a structured
|
|
46
|
+
* outcome instead of swallowing failures. Never throws.
|
|
47
|
+
*
|
|
48
|
+
* Outcomes:
|
|
49
|
+
* - SKIPPED: member has no wixMemberId, nothing to change.
|
|
50
|
+
* - UPDATED: Wix login email changed successfully.
|
|
51
|
+
* - FAILED: change failed. The caller keeps the CMS login email unchanged (so it stays
|
|
52
|
+
* consistent with Wix) and reports the member in the task result for manual handling.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} member - Member with { memberId, wixMemberId, email }
|
|
55
|
+
* @returns {Promise<Object>} outcome
|
|
43
56
|
*/
|
|
44
|
-
async function updateWixMemberLoginEmail(member
|
|
57
|
+
async function updateWixMemberLoginEmail(member) {
|
|
58
|
+
const desiredEmail = member.email;
|
|
59
|
+
const base = { memberId: member.memberId, wixMemberId: member.wixMemberId, desiredEmail };
|
|
60
|
+
|
|
45
61
|
if (!member.wixMemberId) {
|
|
46
|
-
console.log(
|
|
47
|
-
return;
|
|
62
|
+
console.log(`${LOG} member ${member.memberId} has no wixMemberId - skipping`);
|
|
63
|
+
return { ...base, status: LOGIN_EMAIL_SYNC_STATUS.SKIPPED };
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const updatedWixMember = await elevatedChangeLoginEmail(member.wixMemberId, member.email);
|
|
66
|
+
console.log(
|
|
67
|
+
`${LOG} attempting login-email change for member ${member.memberId} (wixMemberId: ${member.wixMemberId}) -> ${desiredEmail}`
|
|
68
|
+
);
|
|
56
69
|
|
|
70
|
+
try {
|
|
71
|
+
const updatedWixMember = await elevatedChangeLoginEmail(member.wixMemberId, desiredEmail);
|
|
57
72
|
console.log(
|
|
58
|
-
|
|
73
|
+
`${LOG} ✅ updated member ${member.memberId} (wixMemberId: ${member.wixMemberId}) -> ${updatedWixMember.loginEmail}`
|
|
59
74
|
);
|
|
60
|
-
|
|
61
|
-
if (!result.wixMemberUpdates) {
|
|
62
|
-
result.wixMemberUpdates = { successful: 0, failed: 0 };
|
|
63
|
-
}
|
|
64
|
-
result.wixMemberUpdates.successful++;
|
|
75
|
+
return { ...base, status: LOGIN_EMAIL_SYNC_STATUS.UPDATED };
|
|
65
76
|
} catch (error) {
|
|
66
|
-
console.error(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
result.wixMemberUpdates.failed++;
|
|
72
|
-
|
|
73
|
-
if (!result.wixMemberErrors) {
|
|
74
|
-
result.wixMemberErrors = [];
|
|
75
|
-
}
|
|
76
|
-
result.wixMemberErrors.push({
|
|
77
|
-
memberId: member.memberId,
|
|
78
|
-
wixMemberId: member.wixMemberId,
|
|
79
|
-
email: member.email,
|
|
80
|
-
error: error.message,
|
|
81
|
-
});
|
|
77
|
+
console.error(
|
|
78
|
+
`${LOG} ❌ login-email change failed for member ${member.memberId} (wixMemberId: ${member.wixMemberId}) -> ${desiredEmail}: ${error.message}`
|
|
79
|
+
);
|
|
80
|
+
return { ...base, status: LOGIN_EMAIL_SYNC_STATUS.FAILED, error: error.message };
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { COLLECTIONS } = require('../public/consts');
|
|
2
|
-
const { isWixHostedImage } = require('../public/Utils/sharedUtils');
|
|
2
|
+
const { isWixHostedImage, emailsMatch, normalizeEmail } = require('../public/Utils/sharedUtils');
|
|
3
3
|
|
|
4
4
|
const { MEMBERSHIPS_TYPES } = require('./consts');
|
|
5
5
|
const { createSiteContact } = require('./contacts-methods');
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
generateGeoHash,
|
|
16
16
|
searchAllItems,
|
|
17
17
|
runIf,
|
|
18
|
+
withTransientErrorRetry,
|
|
18
19
|
} = require('./utils');
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -35,7 +36,42 @@ async function findMemberByWixDataId(memberId) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const hasDifferentEmails = memberData =>
|
|
38
|
-
memberData.contactFormEmail &&
|
|
39
|
+
Boolean(memberData.contactFormEmail) &&
|
|
40
|
+
!emailsMatch(memberData.contactFormEmail, memberData.email);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns a shallow copy of a member record with its email fields normalized
|
|
44
|
+
* (lowercased + trimmed). Wix CRM and our uniqueness checks treat emails
|
|
45
|
+
* case-insensitively, so we persist them in canonical form to keep `.eq` lookups
|
|
46
|
+
* reliable. Only rewrites string values that are actually present.
|
|
47
|
+
* @param {Object} memberData
|
|
48
|
+
* @returns {Object}
|
|
49
|
+
*/
|
|
50
|
+
const normalizeMemberEmailFields = memberData => {
|
|
51
|
+
if (!memberData || typeof memberData !== 'object') return memberData;
|
|
52
|
+
const normalized = { ...memberData };
|
|
53
|
+
if (typeof normalized.email === 'string') {
|
|
54
|
+
normalized.email = normalizeEmail(normalized.email);
|
|
55
|
+
}
|
|
56
|
+
if (typeof normalized.contactFormEmail === 'string') {
|
|
57
|
+
normalized.contactFormEmail = normalizeEmail(normalized.contactFormEmail);
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a member's stored email fields are not already in canonical form
|
|
64
|
+
* (lowercased + trimmed) and therefore need the normalization backfill.
|
|
65
|
+
* @param {Object} member
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
const memberNeedsEmailNormalization = member =>
|
|
69
|
+
(typeof member.email === 'string' &&
|
|
70
|
+
member.email.length > 0 &&
|
|
71
|
+
member.email !== normalizeEmail(member.email)) ||
|
|
72
|
+
(typeof member.contactFormEmail === 'string' &&
|
|
73
|
+
member.contactFormEmail.length > 0 &&
|
|
74
|
+
member.contactFormEmail !== normalizeEmail(member.contactFormEmail));
|
|
39
75
|
|
|
40
76
|
async function createContactAndMemberIfNew(memberData) {
|
|
41
77
|
if (!memberData) {
|
|
@@ -97,8 +133,14 @@ async function bulkSaveMembers(memberDataList, collectionName = COLLECTIONS.MEMB
|
|
|
97
133
|
}
|
|
98
134
|
|
|
99
135
|
try {
|
|
136
|
+
// Normalize email fields only for the members collection; other collections passed here
|
|
137
|
+
// (e.g. staging copies) don't have these fields and must be saved untouched.
|
|
138
|
+
const listToSave =
|
|
139
|
+
collectionName === COLLECTIONS.MEMBERS_DATA
|
|
140
|
+
? memberDataList.map(normalizeMemberEmailFields)
|
|
141
|
+
: memberDataList;
|
|
100
142
|
// bulkSave all with batches of 1000 items as this is the Velo limit for bulkSave
|
|
101
|
-
const batches = chunkArray(
|
|
143
|
+
const batches = chunkArray(listToSave, 1000);
|
|
102
144
|
return await Promise.all(batches.map(batch => wixData.bulkSave(collectionName, batch)));
|
|
103
145
|
} catch (error) {
|
|
104
146
|
console.error('Error bulk saving members:', error);
|
|
@@ -117,11 +159,9 @@ async function findMemberById(memberId) {
|
|
|
117
159
|
}
|
|
118
160
|
|
|
119
161
|
try {
|
|
120
|
-
const queryResult = await
|
|
121
|
-
.query(COLLECTIONS.MEMBERS_DATA)
|
|
122
|
-
|
|
123
|
-
.limit(2)
|
|
124
|
-
.find();
|
|
162
|
+
const queryResult = await withTransientErrorRetry(() =>
|
|
163
|
+
wixData.query(COLLECTIONS.MEMBERS_DATA).eq('memberId', memberId).limit(2).find()
|
|
164
|
+
);
|
|
125
165
|
if (queryResult.items.length > 1) {
|
|
126
166
|
throw new Error(
|
|
127
167
|
`Multiple members found with memberId ${memberId} members _ids are : [${queryResult.items.map(member => member._id).join(', ')}]`
|
|
@@ -134,6 +174,51 @@ async function findMemberById(memberId) {
|
|
|
134
174
|
}
|
|
135
175
|
}
|
|
136
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Retrieves existing members for a list of member IDs in bulk.
|
|
179
|
+
* Uses chunked `hasSome` queries so a full page of members costs a handful of
|
|
180
|
+
* requests instead of one query per member (the per-member fan-out made a whole
|
|
181
|
+
* page fail whenever a single query hit a transient "fetch failed").
|
|
182
|
+
* @param {Array<string|number>} memberIds - Member IDs to look up
|
|
183
|
+
* @returns {Promise<Map<string, Object>>} - Map of String(memberId) to member record (missing IDs are absent)
|
|
184
|
+
*/
|
|
185
|
+
async function findMembersByIds(memberIds) {
|
|
186
|
+
const uniqueIds = [...new Set((memberIds || []).filter(id => id !== undefined && id !== null))];
|
|
187
|
+
const membersById = new Map();
|
|
188
|
+
if (uniqueIds.length === 0) {
|
|
189
|
+
return membersById;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const idChunks = chunkArray(uniqueIds, 100);
|
|
194
|
+
const chunkResults = await Promise.all(
|
|
195
|
+
idChunks.map(idsChunk =>
|
|
196
|
+
withTransientErrorRetry(() =>
|
|
197
|
+
queryAllItems(
|
|
198
|
+
wixData.query(COLLECTIONS.MEMBERS_DATA).hasSome('memberId', idsChunk).limit(1000)
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const duplicateIds = new Set();
|
|
205
|
+
chunkResults.flat().forEach(member => {
|
|
206
|
+
const key = String(member.memberId);
|
|
207
|
+
if (membersById.has(key)) {
|
|
208
|
+
duplicateIds.add(key);
|
|
209
|
+
}
|
|
210
|
+
membersById.set(key, member);
|
|
211
|
+
});
|
|
212
|
+
if (duplicateIds.size > 0) {
|
|
213
|
+
throw new Error(`Multiple members found with memberId(s): [${[...duplicateIds].join(', ')}]`);
|
|
214
|
+
}
|
|
215
|
+
return membersById;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('Error finding members by IDs:', error);
|
|
218
|
+
throw new Error(`Failed to retrieve member data: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
137
222
|
/**
|
|
138
223
|
* Method to get member by slug with flexible filtering options
|
|
139
224
|
* @param {Object} options - Query options
|
|
@@ -244,24 +329,48 @@ const getAllEmptyAboutYouMembers = async () => {
|
|
|
244
329
|
*/
|
|
245
330
|
async function updateMember(memberToUpdate) {
|
|
246
331
|
try {
|
|
247
|
-
const updatedMember = await wixData.update(
|
|
332
|
+
const updatedMember = await wixData.update(
|
|
333
|
+
COLLECTIONS.MEMBERS_DATA,
|
|
334
|
+
normalizeMemberEmailFields(memberToUpdate)
|
|
335
|
+
);
|
|
248
336
|
return updatedMember;
|
|
249
337
|
} catch (error) {
|
|
250
338
|
throw new Error(`Failed to update member data: ${error.message}`);
|
|
251
339
|
}
|
|
252
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Whether the given email is already used by a DIFFERENT member (case-insensitive).
|
|
343
|
+
* Returns false when the email is free or belongs to the same member, so a member can
|
|
344
|
+
* always keep/normalize their own email.
|
|
345
|
+
* @param {string} email
|
|
346
|
+
* @param {string|number} memberId - The member requesting the change
|
|
347
|
+
* @returns {Promise<boolean>}
|
|
348
|
+
*/
|
|
253
349
|
async function isEmailAlreadyUsed(email, memberId) {
|
|
254
350
|
const member = await getMemberByContactEmail(email);
|
|
255
|
-
return member !== null && member.memberId !== memberId;
|
|
351
|
+
return member !== null && String(member.memberId) !== String(memberId);
|
|
256
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Finds the member that owns an email (in either the login or contact-form field),
|
|
355
|
+
* matching case-insensitively. Emails are normalized (lowercased + trimmed) on write and
|
|
356
|
+
* backfilled by the email-normalization migration, so stored values are canonical: a single
|
|
357
|
+
* `.eq` against the normalized email is an exact, case-insensitive lookup.
|
|
358
|
+
* Throws if two DIFFERENT members share the email (a data-integrity violation).
|
|
359
|
+
* @param {string} email
|
|
360
|
+
* @returns {Promise<Object|null>}
|
|
361
|
+
*/
|
|
257
362
|
async function getMemberByContactEmail(email) {
|
|
363
|
+
const normalized = normalizeEmail(email);
|
|
364
|
+
if (!normalized) return null;
|
|
365
|
+
|
|
258
366
|
const members = await wixData
|
|
259
367
|
.query(COLLECTIONS.MEMBERS_DATA)
|
|
260
|
-
.eq('contactFormEmail',
|
|
261
|
-
.or(wixData.query(COLLECTIONS.MEMBERS_DATA).eq('email',
|
|
368
|
+
.eq('contactFormEmail', normalized)
|
|
369
|
+
.or(wixData.query(COLLECTIONS.MEMBERS_DATA).eq('email', normalized))
|
|
262
370
|
.limit(2)
|
|
263
371
|
.find()
|
|
264
372
|
.then(res => res.items);
|
|
373
|
+
|
|
265
374
|
if (members.length > 1) {
|
|
266
375
|
throw new Error(
|
|
267
376
|
`[getMemberByContactEmail] Multiple members found with same loginemail or contactFormEmail ${email} membersIds are : [${members.map(member => member.memberId).join(', ')}]`
|
|
@@ -451,6 +560,29 @@ const getAllMembersWithoutContactFormEmail = async () => {
|
|
|
451
560
|
}
|
|
452
561
|
};
|
|
453
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Gets all members whose email or contactFormEmail is stored with non-canonical casing
|
|
565
|
+
* (or surrounding whitespace) and therefore needs the normalization backfill.
|
|
566
|
+
* Wix Data cannot compare a field to its own lowercase form, so we fetch members that have
|
|
567
|
+
* an email set and filter in memory.
|
|
568
|
+
* @returns {Promise<Array>} - Array of member data
|
|
569
|
+
*/
|
|
570
|
+
const getAllMembersNeedingEmailNormalization = async () => {
|
|
571
|
+
try {
|
|
572
|
+
const membersQuery = wixData
|
|
573
|
+
.query(COLLECTIONS.MEMBERS_DATA)
|
|
574
|
+
.isNotEmpty('email')
|
|
575
|
+
.or(wixData.query(COLLECTIONS.MEMBERS_DATA).isNotEmpty('contactFormEmail'))
|
|
576
|
+
.limit(1000);
|
|
577
|
+
|
|
578
|
+
const allItems = await queryAllItems(membersQuery);
|
|
579
|
+
return allItems.filter(memberNeedsEmailNormalization);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
console.error('Error getting members needing email normalization:', error);
|
|
582
|
+
throw new Error(`Failed to get members needing email normalization: ${error.message}`);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
454
586
|
/* Gets all updated login emails from the updated emails database
|
|
455
587
|
* @returns {Promise<Array>} - Array of updated email data
|
|
456
588
|
*/
|
|
@@ -488,9 +620,13 @@ const getMembersByIds = async memberIds => {
|
|
|
488
620
|
|
|
489
621
|
const getMemberByEmail = async email => {
|
|
490
622
|
try {
|
|
623
|
+
// Login emails are normalized on write, so match against the normalized value (see
|
|
624
|
+
// getMemberByContactEmail) — an exact `.eq` is a case-insensitive lookup.
|
|
625
|
+
const normalized = normalizeEmail(email);
|
|
626
|
+
if (!normalized) return null;
|
|
491
627
|
const members = await wixData
|
|
492
628
|
.query(COLLECTIONS.MEMBERS_DATA)
|
|
493
|
-
.eq('email',
|
|
629
|
+
.eq('email', normalized)
|
|
494
630
|
.limit(2)
|
|
495
631
|
.find()
|
|
496
632
|
.then(res => res.items);
|
|
@@ -631,6 +767,7 @@ module.exports = {
|
|
|
631
767
|
saveRegistrationData,
|
|
632
768
|
bulkSaveMembers,
|
|
633
769
|
findMemberById,
|
|
770
|
+
findMembersByIds,
|
|
634
771
|
getMemberBySlug,
|
|
635
772
|
getCMSMemberByWixMemberId,
|
|
636
773
|
getAllEmptyAboutYouMembers,
|
|
@@ -638,6 +775,8 @@ module.exports = {
|
|
|
638
775
|
getAllMembersWithExternalImages,
|
|
639
776
|
getMembersWithWixUrl,
|
|
640
777
|
getAllMembersWithoutContactFormEmail,
|
|
778
|
+
getAllMembersNeedingEmailNormalization,
|
|
779
|
+
memberNeedsEmailNormalization,
|
|
641
780
|
getAllUpdatedLoginEmails,
|
|
642
781
|
getMembersByIds,
|
|
643
782
|
getMemberByEmail,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
1
3
|
const { PAC_API_URL, TEST_PAC_API_URL, BACKUP_API_URL } = require('./consts');
|
|
2
4
|
const { getSecret } = require('./utils');
|
|
3
5
|
|
|
@@ -31,24 +33,22 @@ const fetchPACMembers = async ({ page, action, backupDate, isTestEnvironment })
|
|
|
31
33
|
const url = `${baseUrl}/Members?${new URLSearchParams(queryParams).toString()}`;
|
|
32
34
|
console.log(`Fetching PAC members from: ${url}`);
|
|
33
35
|
const headers = await getHeaders();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
const
|
|
39
|
-
const responseType = response.headers.get('content-type');
|
|
36
|
+
const response = await axios.get(url, {
|
|
37
|
+
headers,
|
|
38
|
+
validateStatus: () => true,
|
|
39
|
+
});
|
|
40
|
+
const responseType = response.headers['content-type'] || '';
|
|
40
41
|
if (!responseType.includes('application/json')) {
|
|
41
42
|
const errorMessage = `[fetchPACMembers] got invalid responseType: ${responseType} for page ${page} and actionFilter ${action}`;
|
|
42
43
|
console.error(errorMessage);
|
|
43
44
|
throw new Error(errorMessage);
|
|
44
45
|
}
|
|
45
|
-
if (response.
|
|
46
|
-
return response.
|
|
47
|
-
} else {
|
|
48
|
-
const errorMessage = `[fetchPACMembers] failed with status ${response.status} for page ${page} and actionFilter ${action}`;
|
|
49
|
-
console.error(errorMessage);
|
|
50
|
-
throw new Error(errorMessage);
|
|
46
|
+
if (response.status >= 200 && response.status < 300) {
|
|
47
|
+
return response.data;
|
|
51
48
|
}
|
|
49
|
+
const errorMessage = `[fetchPACMembers] failed with status ${response.status} for page ${page} and actionFilter ${action}`;
|
|
50
|
+
console.error(errorMessage);
|
|
51
|
+
throw new Error(errorMessage);
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
module.exports = { fetchPACMembers, getHeaders }; //TODO: remove getHeaders from exported methods once npm movement finishes
|
package/backend/tasks/consts.js
CHANGED
|
@@ -22,6 +22,8 @@ const TASKS_NAMES = {
|
|
|
22
22
|
fixPrimaryAddressChunk: 'fixPrimaryAddressChunk',
|
|
23
23
|
scheduleFixUrlsWithSpaces: 'scheduleFixUrlsWithSpaces',
|
|
24
24
|
fixUrlsWithSpacesChunk: 'fixUrlsWithSpacesChunk',
|
|
25
|
+
scheduleNormalizeMemberEmails: 'scheduleNormalizeMemberEmails',
|
|
26
|
+
normalizeMemberEmailsChunk: 'normalizeMemberEmailsChunk',
|
|
25
27
|
dailyPullExecutionCheck: 'dailyPullExecutionCheck',
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { taskManager } = require('psdev-task-manager');
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
bulkSaveMembers,
|
|
5
|
+
getMembersByIds,
|
|
6
|
+
getAllMembersNeedingEmailNormalization,
|
|
7
|
+
memberNeedsEmailNormalization,
|
|
8
|
+
} = require('../members-data-methods');
|
|
9
|
+
const { chunkArray } = require('../utils');
|
|
10
|
+
|
|
11
|
+
const { TASKS_NAMES } = require('./consts');
|
|
12
|
+
|
|
13
|
+
const CHUNK_SIZE = 1000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One-off backfill: schedules tasks to normalize (lowercase + trim) the email and
|
|
17
|
+
* contactFormEmail fields of existing members so stored values match the normalize-on-write
|
|
18
|
+
* behavior, keeping case-insensitive uniqueness lookups reliable.
|
|
19
|
+
*/
|
|
20
|
+
async function scheduleNormalizeMemberEmails() {
|
|
21
|
+
console.log('=== Scheduling Member Email Normalization ===');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const members = await getAllMembersNeedingEmailNormalization();
|
|
25
|
+
console.log(`Fetched ${members.length} members needing email normalization`);
|
|
26
|
+
|
|
27
|
+
const memberIds = [
|
|
28
|
+
...new Set(
|
|
29
|
+
members
|
|
30
|
+
.map(member => Number(member.memberId))
|
|
31
|
+
.filter(memberId => Number.isFinite(memberId) && memberId > 0)
|
|
32
|
+
),
|
|
33
|
+
];
|
|
34
|
+
console.log(`Members to normalize: ${memberIds.length}`);
|
|
35
|
+
|
|
36
|
+
if (memberIds.length === 0) {
|
|
37
|
+
console.log('No members need email normalization');
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
message: 'No members need email normalization',
|
|
41
|
+
totalMembers: 0,
|
|
42
|
+
tasksScheduled: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const chunks = chunkArray(memberIds, CHUNK_SIZE);
|
|
47
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
48
|
+
const chunk = chunks[i];
|
|
49
|
+
const task = {
|
|
50
|
+
name: TASKS_NAMES.normalizeMemberEmailsChunk,
|
|
51
|
+
data: {
|
|
52
|
+
memberIds: chunk,
|
|
53
|
+
chunkIndex: i,
|
|
54
|
+
totalChunks: chunks.length,
|
|
55
|
+
},
|
|
56
|
+
type: 'scheduled',
|
|
57
|
+
};
|
|
58
|
+
await taskManager().schedule(task);
|
|
59
|
+
console.log(`Scheduled task ${i + 1}/${chunks.length} (${chunk.length} members)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = {
|
|
63
|
+
success: true,
|
|
64
|
+
message: `Scheduled ${chunks.length} tasks for ${memberIds.length} members`,
|
|
65
|
+
totalMembers: memberIds.length,
|
|
66
|
+
tasksScheduled: chunks.length,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
console.log('=== Scheduling Complete ===');
|
|
70
|
+
console.log(JSON.stringify(result, null, 2));
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Error scheduling member email normalization:', error);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Processes a chunk of members, normalizing their email fields.
|
|
81
|
+
* bulkSaveMembers normalizes on write, so we only need to select the members that still
|
|
82
|
+
* have non-canonical values and save them.
|
|
83
|
+
*/
|
|
84
|
+
async function normalizeMemberEmailsChunk(data) {
|
|
85
|
+
const { memberIds, chunkIndex, totalChunks } = data;
|
|
86
|
+
console.log(
|
|
87
|
+
`Processing email normalization chunk ${chunkIndex + 1}/${totalChunks} (${memberIds.length} members)`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const result = {
|
|
91
|
+
successful: 0,
|
|
92
|
+
failed: 0,
|
|
93
|
+
skipped: 0,
|
|
94
|
+
errors: [],
|
|
95
|
+
failedIds: [],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const members = await getMembersByIds(memberIds);
|
|
100
|
+
console.log(`Loaded ${members.length} members for this chunk`);
|
|
101
|
+
|
|
102
|
+
const membersToUpdate = members.filter(memberNeedsEmailNormalization);
|
|
103
|
+
result.skipped = members.length - membersToUpdate.length;
|
|
104
|
+
|
|
105
|
+
if (membersToUpdate.length === 0) {
|
|
106
|
+
console.log('No members need updating in this batch');
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await bulkSaveMembers(membersToUpdate);
|
|
112
|
+
result.successful += membersToUpdate.length;
|
|
113
|
+
console.log(`✅ Successfully normalized ${membersToUpdate.length} members`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('❌ Error bulk saving members:', error);
|
|
116
|
+
result.failed += membersToUpdate.length;
|
|
117
|
+
result.failedIds.push(...membersToUpdate.map(member => member.memberId));
|
|
118
|
+
result.errors.push({
|
|
119
|
+
error: error.message,
|
|
120
|
+
memberCount: membersToUpdate.length,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`Error processing email normalization chunk ${chunkIndex}:`, error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
scheduleNormalizeMemberEmails,
|
|
133
|
+
normalizeMemberEmailsChunk,
|
|
134
|
+
};
|
|
@@ -10,6 +10,10 @@ const {
|
|
|
10
10
|
} = require('./address-primary-methods');
|
|
11
11
|
const { TASKS_NAMES } = require('./consts');
|
|
12
12
|
const { dailyPullExecutionCheck } = require('./daily-pull-check-methods');
|
|
13
|
+
const {
|
|
14
|
+
scheduleNormalizeMemberEmails,
|
|
15
|
+
normalizeMemberEmailsChunk,
|
|
16
|
+
} = require('./email-normalize-methods');
|
|
13
17
|
const {
|
|
14
18
|
scheduleTaskForEmptyAboutYouMembers,
|
|
15
19
|
convertAboutYouHtmlToRichContent,
|
|
@@ -203,6 +207,20 @@ const TASKS = {
|
|
|
203
207
|
shouldSkipCheck: () => false,
|
|
204
208
|
estimatedDurationSec: 80,
|
|
205
209
|
},
|
|
210
|
+
[TASKS_NAMES.scheduleNormalizeMemberEmails]: {
|
|
211
|
+
name: TASKS_NAMES.scheduleNormalizeMemberEmails,
|
|
212
|
+
getIdentifier: () => 'SHOULD_NEVER_SKIP',
|
|
213
|
+
process: scheduleNormalizeMemberEmails,
|
|
214
|
+
shouldSkipCheck: () => false,
|
|
215
|
+
estimatedDurationSec: 80,
|
|
216
|
+
},
|
|
217
|
+
[TASKS_NAMES.normalizeMemberEmailsChunk]: {
|
|
218
|
+
name: TASKS_NAMES.normalizeMemberEmailsChunk,
|
|
219
|
+
getIdentifier: task => task.data,
|
|
220
|
+
process: normalizeMemberEmailsChunk,
|
|
221
|
+
shouldSkipCheck: () => false,
|
|
222
|
+
estimatedDurationSec: 80,
|
|
223
|
+
},
|
|
206
224
|
[TASKS_NAMES.dailyPullExecutionCheck]: {
|
|
207
225
|
name: TASKS_NAMES.dailyPullExecutionCheck,
|
|
208
226
|
getIdentifier: task => task.data,
|
|
@@ -103,23 +103,23 @@ async function updateMemberRichContent(memberId) {
|
|
|
103
103
|
content: htmlString,
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Cookie: 'XSRF-TOKEN=1753949844|p--a7HsuVjR4',
|
|
111
|
-
Authorization: 'Bearer ' + (await getServerlessAuth()),
|
|
112
|
-
},
|
|
113
|
-
body: raw,
|
|
106
|
+
const requestHeaders = {
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
Cookie: 'XSRF-TOKEN=1753949844|p--a7HsuVjR4',
|
|
109
|
+
Authorization: 'Bearer ' + (await getServerlessAuth()),
|
|
114
110
|
};
|
|
115
111
|
|
|
116
112
|
try {
|
|
117
|
-
const response = await
|
|
113
|
+
const response = await axios.post(
|
|
118
114
|
'https://www.wixapis.com/data-sync/v1/abmp-content-converter',
|
|
119
|
-
|
|
115
|
+
raw,
|
|
116
|
+
{
|
|
117
|
+
headers: requestHeaders,
|
|
118
|
+
validateStatus: () => true,
|
|
119
|
+
}
|
|
120
120
|
);
|
|
121
|
-
if (response.
|
|
122
|
-
const data =
|
|
121
|
+
if (response.status >= 200 && response.status < 300) {
|
|
122
|
+
const data = response.data;
|
|
123
123
|
const updatedMember = {
|
|
124
124
|
...member,
|
|
125
125
|
aboutYourSelf: data.richContent.richContent,
|
|
@@ -358,9 +358,13 @@ async function uploadMembersSitemap({ members, tokens, destinationFileName, site
|
|
|
358
358
|
|
|
359
359
|
const url = `https://${host}${pathName}`;
|
|
360
360
|
console.log('url', url);
|
|
361
|
-
const res = await
|
|
362
|
-
|
|
363
|
-
|
|
361
|
+
const res = await axios.put(url, body, {
|
|
362
|
+
headers: reqOpts.headers,
|
|
363
|
+
transformResponse: [d => d],
|
|
364
|
+
validateStatus: () => true,
|
|
365
|
+
});
|
|
366
|
+
if (res.status < 200 || res.status >= 300) {
|
|
367
|
+
const respText = res.data;
|
|
364
368
|
console.log('Response body', respText);
|
|
365
369
|
throw new Error(`S3 PUT failed ${res.status} ${res.statusText}: ${respText}`);
|
|
366
370
|
}
|
|
@@ -391,13 +395,13 @@ async function stsPost(body, baseAccessKeyId, baseSecretAccessKey) {
|
|
|
391
395
|
accessKeyId: baseAccessKeyId,
|
|
392
396
|
secretAccessKey: baseSecretAccessKey,
|
|
393
397
|
});
|
|
394
|
-
const res = await
|
|
395
|
-
method,
|
|
398
|
+
const res = await axios.post(`https://${host}${path}`, body, {
|
|
396
399
|
headers: reqOpts.headers,
|
|
397
|
-
|
|
400
|
+
transformResponse: [d => d],
|
|
401
|
+
validateStatus: () => true,
|
|
398
402
|
});
|
|
399
|
-
const text =
|
|
400
|
-
if (
|
|
403
|
+
const text = res.data;
|
|
404
|
+
if (res.status < 200 || res.status >= 300) throw new Error(`STS ${res.status}: ${text}`);
|
|
401
405
|
|
|
402
406
|
const accessKeyId = parseXmlVal(text, 'AccessKeyId');
|
|
403
407
|
const secretAccessKey = parseXmlVal(text, 'SecretAccessKey');
|