abmp-npm 1.10.4 → 1.10.5
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/daily-pull/bulk-process-methods.js +126 -6
- package/backend/daily-pull/index.js +1 -0
- package/backend/daily-pull/process-member-methods.js +2 -3
- package/backend/daily-pull/utils.js +33 -0
- package/backend/dev-only-methods.js +18 -0
- package/backend/index.js +1 -0
- package/backend/members-data-methods.js +10 -21
- package/backend/tasks/consts.js +0 -4
- package/backend/tasks/migration-methods.js +0 -20
- package/backend/tasks/tasks-configs.js +0 -34
- package/backend/utils.js +0 -2
- package/dev-only-scripts/find-duplicate-urls.js +202 -0
- package/package.json +4 -2
- package/pages/Profile.js +31 -31
- package/pages/personalDetails.js +8 -0
- package/backend/tasks/url-migration-methods.js +0 -376
|
@@ -1,7 +1,123 @@
|
|
|
1
|
-
const { bulkSaveMembers } = require('../members-data-methods');
|
|
1
|
+
const { bulkSaveMembers, getMemberBySlug } = require('../members-data-methods');
|
|
2
2
|
|
|
3
3
|
const { generateUpdatedMemberData } = require('./process-member-methods');
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
changeWixMembersEmails,
|
|
6
|
+
extractUrlCounter,
|
|
7
|
+
incrementUrlCounter,
|
|
8
|
+
extractBaseUrl,
|
|
9
|
+
} = require('./utils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ensures unique URLs within a batch of members by deduplicating URLs
|
|
13
|
+
* Groups members by their base URL (normalized) and assigns unique counters
|
|
14
|
+
* Also checks database to handle cross-page conflicts
|
|
15
|
+
* @param {Array} memberDataList - Array of processed member data
|
|
16
|
+
* @returns {Promise<Array>} - Array of members with unique URLs assigned
|
|
17
|
+
*/
|
|
18
|
+
async function ensureUniqueUrlsInBatch(memberDataList) {
|
|
19
|
+
if (!Array.isArray(memberDataList) || memberDataList.length === 0) {
|
|
20
|
+
return memberDataList;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Group members by their normalized base URL
|
|
24
|
+
const urlGroups = new Map();
|
|
25
|
+
|
|
26
|
+
memberDataList.forEach(member => {
|
|
27
|
+
if (!member || !member.url) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract the base URL (without any counter) for grouping
|
|
32
|
+
const baseUrl = extractBaseUrl(member.url);
|
|
33
|
+
if (!urlGroups.has(baseUrl)) {
|
|
34
|
+
urlGroups.set(baseUrl, []);
|
|
35
|
+
}
|
|
36
|
+
urlGroups.get(baseUrl).push(member);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// For each group, check database and assign unique URLs sequentially
|
|
40
|
+
for (const [baseUrl, members] of urlGroups.entries()) {
|
|
41
|
+
if (members.length <= 1) {
|
|
42
|
+
// Single member - still check DB to ensure it doesn't conflict with other pages
|
|
43
|
+
const member = members[0];
|
|
44
|
+
if (member) {
|
|
45
|
+
const dbMember = await getMemberBySlug({
|
|
46
|
+
slug: baseUrl,
|
|
47
|
+
excludeDropped: false,
|
|
48
|
+
normalizeSlugForComparison: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (dbMember && dbMember.url) {
|
|
52
|
+
// Conflict found in DB, need to add counter
|
|
53
|
+
member.url = incrementUrlCounter(dbMember.url, baseUrl);
|
|
54
|
+
console.log(
|
|
55
|
+
`Found DB conflict for single member with base URL "${baseUrl}", assigned: ${member.url}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sort members to ensure consistent ordering
|
|
63
|
+
members.sort((a, b) => {
|
|
64
|
+
if (a.url && b.url) {
|
|
65
|
+
return String(a.url).localeCompare(String(b.url));
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Check database for existing members with this base URL to find highest counter
|
|
71
|
+
const dbMember = await getMemberBySlug({
|
|
72
|
+
slug: baseUrl,
|
|
73
|
+
excludeDropped: false,
|
|
74
|
+
normalizeSlugForComparison: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const dbMaxCounter = extractUrlCounter(dbMember?.url);
|
|
78
|
+
|
|
79
|
+
// Find the highest existing counter among all members in this batch group
|
|
80
|
+
let batchMaxCounter = -1;
|
|
81
|
+
members.forEach(member => {
|
|
82
|
+
const originalUrl = member.url;
|
|
83
|
+
const urlParts = originalUrl.split('-');
|
|
84
|
+
const lastSegment = urlParts[urlParts.length - 1];
|
|
85
|
+
const isNumeric = /^\d+$/.test(lastSegment);
|
|
86
|
+
if (isNumeric) {
|
|
87
|
+
const counter = parseInt(lastSegment, 10);
|
|
88
|
+
if (counter > batchMaxCounter) {
|
|
89
|
+
batchMaxCounter = counter;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Start index from the maximum of DB counter and batch counter + 1
|
|
95
|
+
const maxCounter = Math.max(dbMaxCounter, batchMaxCounter);
|
|
96
|
+
const startIndex = maxCounter + 1;
|
|
97
|
+
|
|
98
|
+
// Assign unique URLs: start from the appropriate index
|
|
99
|
+
members.forEach((member, index) => {
|
|
100
|
+
const assignedIndex = startIndex + index;
|
|
101
|
+
if (assignedIndex === 0) {
|
|
102
|
+
// Index 0 means no counter, use baseUrl
|
|
103
|
+
member.url = baseUrl;
|
|
104
|
+
} else {
|
|
105
|
+
// Index > 0 means add counter
|
|
106
|
+
member.url = `${baseUrl}-${assignedIndex}`;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(
|
|
111
|
+
`Deduplicated ${
|
|
112
|
+
members.length
|
|
113
|
+
} members with base URL "${baseUrl}" (DB max: ${dbMaxCounter}, batch max: ${batchMaxCounter}, start: ${startIndex}): ${members
|
|
114
|
+
.map(m => m.url)
|
|
115
|
+
.join(', ')}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return memberDataList;
|
|
120
|
+
}
|
|
5
121
|
|
|
6
122
|
/**
|
|
7
123
|
* Processes and saves multiple member records in bulk
|
|
@@ -36,7 +152,6 @@ const bulkProcessAndSaveMemberData = async ({
|
|
|
36
152
|
const validMemberData = processedMemberDataList.filter(
|
|
37
153
|
data => data !== null && data !== undefined
|
|
38
154
|
);
|
|
39
|
-
|
|
40
155
|
if (validMemberData.length === 0) {
|
|
41
156
|
return {
|
|
42
157
|
totalProcessed: memberDataList.length,
|
|
@@ -45,9 +160,14 @@ const bulkProcessAndSaveMemberData = async ({
|
|
|
45
160
|
processingTime: Date.now() - startTime,
|
|
46
161
|
};
|
|
47
162
|
}
|
|
163
|
+
const newMembers = validMemberData.filter(data => data.isNewToDb);
|
|
164
|
+
const existingMembers = validMemberData.filter(data => !data.isNewToDb);
|
|
165
|
+
// Ensure unique URLs within the batch to prevent duplicates (also checks DB for cross-page conflicts)
|
|
166
|
+
const uniqueUrlsNewToDBMembersList = await ensureUniqueUrlsInBatch(newMembers);
|
|
167
|
+
const uniqueUrlsMembersData = [...uniqueUrlsNewToDBMembersList, ...existingMembers];
|
|
48
168
|
const toChangeWixMembersEmails = [];
|
|
49
|
-
const toSaveMembersData =
|
|
50
|
-
const { isLoginEmailChanged, ...restMemberData } = member;
|
|
169
|
+
const toSaveMembersData = uniqueUrlsMembersData.map(member => {
|
|
170
|
+
const { isLoginEmailChanged, isNewToDb: _isNewToDb, ...restMemberData } = member;
|
|
51
171
|
if (member.contactId && isLoginEmailChanged) {
|
|
52
172
|
toChangeWixMembersEmails.push(member);
|
|
53
173
|
}
|
|
@@ -73,4 +193,4 @@ const bulkProcessAndSaveMemberData = async ({
|
|
|
73
193
|
}
|
|
74
194
|
};
|
|
75
195
|
|
|
76
|
-
module.exports = { bulkProcessAndSaveMemberData };
|
|
196
|
+
module.exports = { bulkProcessAndSaveMemberData, ensureUniqueUrlsInBatch };
|
|
@@ -39,7 +39,7 @@ const ensureUniqueUrl = async ({ url, memberId, fullName }) => {
|
|
|
39
39
|
|
|
40
40
|
const existingMember = await getMemberBySlug({
|
|
41
41
|
slug: uniqueUrl,
|
|
42
|
-
excludeDropped:
|
|
42
|
+
excludeDropped: false,
|
|
43
43
|
excludeSearchedMember: true,
|
|
44
44
|
memberId,
|
|
45
45
|
normalizeSlugForComparison: true,
|
|
@@ -105,7 +105,7 @@ async function generateUpdatedMemberData({
|
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
return updatedMemberData;
|
|
108
|
+
return { ...updatedMemberData, isNewToDb: !existingDbMember };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -300,5 +300,4 @@ async function createCoreMemberData(inputMemberData, existingDbMember, currentPa
|
|
|
300
300
|
|
|
301
301
|
module.exports = {
|
|
302
302
|
generateUpdatedMemberData,
|
|
303
|
-
ensureUniqueUrl,
|
|
304
303
|
};
|
|
@@ -15,6 +15,36 @@ const changeWixMembersEmails = async toChangeWixMembersEmails => {
|
|
|
15
15
|
);
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const extractUrlCounter = url => {
|
|
19
|
+
if (!url) return -1;
|
|
20
|
+
const lastSegment = url.split('-').pop() || '0';
|
|
21
|
+
const isNumeric = /^\d+$/.test(lastSegment);
|
|
22
|
+
return isNumeric ? parseInt(lastSegment, 10) : -1;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const extractBaseUrl = url => {
|
|
26
|
+
if (!url) return url;
|
|
27
|
+
const urlParts = url.split('-');
|
|
28
|
+
const lastSegment = urlParts[urlParts.length - 1];
|
|
29
|
+
const isNumeric = /^\d+$/.test(lastSegment);
|
|
30
|
+
if (isNumeric && urlParts.length > 1) {
|
|
31
|
+
// Remove the numeric counter to get the base URL
|
|
32
|
+
return urlParts.slice(0, -1).join('-');
|
|
33
|
+
}
|
|
34
|
+
// No counter found, return the URL as-is
|
|
35
|
+
return url;
|
|
36
|
+
};
|
|
37
|
+
const incrementUrlCounter = (existingUrl, baseUrl) => {
|
|
38
|
+
if (existingUrl && existingUrl === baseUrl) {
|
|
39
|
+
console.log(
|
|
40
|
+
`Found member with same url ${existingUrl} for baseUrl ${baseUrl}, increasing counter by 1`
|
|
41
|
+
);
|
|
42
|
+
const lastSegment = existingUrl.split('-').pop() || '0';
|
|
43
|
+
const isNumeric = /^\d+$/.test(lastSegment);
|
|
44
|
+
const lastCounter = isNumeric ? parseInt(lastSegment, 10) : 0;
|
|
45
|
+
return `${baseUrl}-${lastCounter + 1}`;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
18
48
|
/**
|
|
19
49
|
* Validates core member data requirements
|
|
20
50
|
* @param {Object} inputMemberData - Raw member data from API to validate
|
|
@@ -75,4 +105,7 @@ module.exports = {
|
|
|
75
105
|
validateCoreMemberData,
|
|
76
106
|
containsNonEnglish,
|
|
77
107
|
createFullName,
|
|
108
|
+
extractUrlCounter,
|
|
109
|
+
incrementUrlCounter,
|
|
110
|
+
extractBaseUrl,
|
|
78
111
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { ensureUniqueUrlsInBatch } = require('./daily-pull/bulk-process-methods');
|
|
2
|
+
const { wixData } = require('./elevated-modules');
|
|
3
|
+
const { bulkSaveMembers } = require('./members-data-methods');
|
|
4
|
+
const { queryAllItems } = require('./utils');
|
|
5
|
+
|
|
6
|
+
async function deduplicateURls(collectionName, duplicateUrlsList) {
|
|
7
|
+
const query = await wixData.query(collectionName).hasSome('url', duplicateUrlsList).limit(1000);
|
|
8
|
+
const membersWithSameUrl = await queryAllItems(query);
|
|
9
|
+
|
|
10
|
+
console.log({ membersWithSameUrl });
|
|
11
|
+
const membersWithUniqueUrls = await ensureUniqueUrlsInBatch(membersWithSameUrl);
|
|
12
|
+
console.log({ membersWithUniqueUrls });
|
|
13
|
+
const deduplicatedUrls = membersWithUniqueUrls.map(m => m.url);
|
|
14
|
+
console.log({ deduplicatedUrls });
|
|
15
|
+
return await bulkSaveMembers(membersWithUniqueUrls, collectionName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { deduplicateURls };
|
package/backend/index.js
CHANGED
|
@@ -61,9 +61,10 @@ async function createContactAndMemberIfNew(memberData) {
|
|
|
61
61
|
|
|
62
62
|
/** Performs bulk save operation for member data
|
|
63
63
|
* @param { Array } memberDataList - Array of member data objects to save
|
|
64
|
+
* @param { string } [collectionName] - The collection name to save the members to (default: COLLECTIONS.MEMBERS_DATA)
|
|
64
65
|
* @returns { Promise < Object >} - Bulk save operation result
|
|
65
66
|
*/
|
|
66
|
-
async function bulkSaveMembers(memberDataList) {
|
|
67
|
+
async function bulkSaveMembers(memberDataList, collectionName = COLLECTIONS.MEMBERS_DATA) {
|
|
67
68
|
if (!Array.isArray(memberDataList) || memberDataList.length === 0) {
|
|
68
69
|
throw new Error('Invalid member data list provided for bulk save');
|
|
69
70
|
}
|
|
@@ -71,9 +72,7 @@ async function bulkSaveMembers(memberDataList) {
|
|
|
71
72
|
try {
|
|
72
73
|
// bulkSave all with batches of 1000 items as this is the Velo limit for bulkSave
|
|
73
74
|
const batches = chunkArray(memberDataList, 1000);
|
|
74
|
-
return await Promise.all(
|
|
75
|
-
batches.map(batch => wixData.bulkSave(COLLECTIONS.MEMBERS_DATA, batch))
|
|
76
|
-
);
|
|
75
|
+
return await Promise.all(batches.map(batch => wixData.bulkSave(collectionName, batch)));
|
|
77
76
|
} catch (error) {
|
|
78
77
|
console.error('Error bulk saving members:', error);
|
|
79
78
|
throw new Error(`Bulk save failed: ${error.message}`);
|
|
@@ -270,23 +269,13 @@ async function urlExists(url, excludeMemberId) {
|
|
|
270
269
|
if (!url) return false;
|
|
271
270
|
|
|
272
271
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const { items } = await query.find();
|
|
283
|
-
|
|
284
|
-
// Case-insensitive comparison
|
|
285
|
-
const matchingMembers = items.filter(
|
|
286
|
-
item => item.url && item.url.toLowerCase() === url.toLowerCase()
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
return matchingMembers.length > 0;
|
|
272
|
+
const member = await getMemberBySlug({
|
|
273
|
+
slug: url,
|
|
274
|
+
excludeDropped: false,
|
|
275
|
+
excludeSearchedMember: true,
|
|
276
|
+
memberId: excludeMemberId,
|
|
277
|
+
});
|
|
278
|
+
return member !== null;
|
|
290
279
|
} catch (error) {
|
|
291
280
|
console.error('Error checking URL existence:', error);
|
|
292
281
|
return false;
|
package/backend/tasks/consts.js
CHANGED
|
@@ -12,10 +12,6 @@ const TASKS_NAMES = {
|
|
|
12
12
|
syncMemberLoginEmails: 'syncMemberLoginEmails',
|
|
13
13
|
scheduleContactFormEmailMigration: 'scheduleContactFormEmailMigration',
|
|
14
14
|
migrateContactFormEmails: 'migrateContactFormEmails',
|
|
15
|
-
scheduleMigrateExistingUrls: 'scheduleMigrateExistingUrls',
|
|
16
|
-
migrateUrlsChunk: 'migrateUrlsChunk',
|
|
17
|
-
scheduleGenerateMissingUrls: 'scheduleGenerateMissingUrls',
|
|
18
|
-
generateUrlsChunk: 'generateUrlsChunk',
|
|
19
15
|
};
|
|
20
16
|
|
|
21
17
|
module.exports = {
|
|
@@ -20,27 +20,7 @@ function scheduleExternalProfileImageMigration() {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// Schedule URL migration from backup collection
|
|
24
|
-
function scheduleUrlMigration() {
|
|
25
|
-
return taskManager().schedule({
|
|
26
|
-
name: TASKS_NAMES.scheduleMigrateExistingUrls,
|
|
27
|
-
data: {},
|
|
28
|
-
type: 'scheduled',
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Schedule URL generation for members without URLs
|
|
33
|
-
function scheduleUrlGeneration() {
|
|
34
|
-
return taskManager().schedule({
|
|
35
|
-
name: TASKS_NAMES.scheduleGenerateMissingUrls,
|
|
36
|
-
data: {},
|
|
37
|
-
type: 'scheduled',
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
23
|
module.exports = {
|
|
42
24
|
scheduleConvertHtmlToRichContent,
|
|
43
25
|
scheduleExternalProfileImageMigration,
|
|
44
|
-
scheduleUrlMigration,
|
|
45
|
-
scheduleUrlGeneration,
|
|
46
26
|
};
|
|
@@ -17,12 +17,6 @@ const {
|
|
|
17
17
|
scheduleEmailSync,
|
|
18
18
|
syncMemberLoginEmails,
|
|
19
19
|
} = require('./tasks-process-methods');
|
|
20
|
-
const {
|
|
21
|
-
scheduleMigrateExistingUrls,
|
|
22
|
-
migrateUrlsChunk,
|
|
23
|
-
scheduleGenerateMissingUrls,
|
|
24
|
-
generateUrlsChunk,
|
|
25
|
-
} = require('./url-migration-methods');
|
|
26
20
|
|
|
27
21
|
const getDailyMembersDataSyncChildTasks = () => {
|
|
28
22
|
// we don't want to sync none action as it means this members data hasn't changed and we don't need to sync it
|
|
@@ -125,34 +119,6 @@ const TASKS = {
|
|
|
125
119
|
shouldSkipCheck: () => false,
|
|
126
120
|
estimatedDurationSec: 45,
|
|
127
121
|
},
|
|
128
|
-
[TASKS_NAMES.scheduleMigrateExistingUrls]: {
|
|
129
|
-
name: TASKS_NAMES.scheduleMigrateExistingUrls,
|
|
130
|
-
getIdentifier: () => 'SHOULD_NEVER_SKIP',
|
|
131
|
-
process: scheduleMigrateExistingUrls,
|
|
132
|
-
shouldSkipCheck: () => false,
|
|
133
|
-
estimatedDurationSec: 160,
|
|
134
|
-
},
|
|
135
|
-
[TASKS_NAMES.migrateUrlsChunk]: {
|
|
136
|
-
name: TASKS_NAMES.migrateUrlsChunk,
|
|
137
|
-
getIdentifier: task => `chunk-${task.data.chunkIndex}`,
|
|
138
|
-
process: migrateUrlsChunk,
|
|
139
|
-
shouldSkipCheck: () => false,
|
|
140
|
-
estimatedDurationSec: 80,
|
|
141
|
-
},
|
|
142
|
-
[TASKS_NAMES.scheduleGenerateMissingUrls]: {
|
|
143
|
-
name: TASKS_NAMES.scheduleGenerateMissingUrls,
|
|
144
|
-
getIdentifier: () => 'SHOULD_NEVER_SKIP',
|
|
145
|
-
process: scheduleGenerateMissingUrls,
|
|
146
|
-
shouldSkipCheck: () => false,
|
|
147
|
-
estimatedDurationSec: 160,
|
|
148
|
-
},
|
|
149
|
-
[TASKS_NAMES.generateUrlsChunk]: {
|
|
150
|
-
name: TASKS_NAMES.generateUrlsChunk,
|
|
151
|
-
getIdentifier: task => `chunk-${task.data.chunkIndex}`,
|
|
152
|
-
process: generateUrlsChunk,
|
|
153
|
-
shouldSkipCheck: () => false,
|
|
154
|
-
estimatedDurationSec: 80,
|
|
155
|
-
},
|
|
156
122
|
};
|
|
157
123
|
|
|
158
124
|
module.exports = { TASKS };
|
package/backend/utils.js
CHANGED
|
@@ -111,10 +111,8 @@ const getAllItems = async querySearchResult => {
|
|
|
111
111
|
let oldResults = querySearchResult;
|
|
112
112
|
console.log(`found items: ${oldResults.items.length}`);
|
|
113
113
|
const allItems = oldResults.items;
|
|
114
|
-
console.log(`total pages: ${oldResults.totalPages}`);
|
|
115
114
|
while (oldResults.hasNext()) {
|
|
116
115
|
oldResults = await oldResults.next();
|
|
117
|
-
console.log(`next page: ${oldResults.currentPage}`);
|
|
118
116
|
allItems.push(...oldResults.items);
|
|
119
117
|
}
|
|
120
118
|
console.log(`all items count : ${allItems.length}`);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line import/no-unresolved
|
|
5
|
+
const csv = require('csv-parser');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Finds duplicate URLs in a CSV file and generates a JSON report
|
|
9
|
+
* Usage: node scripts/find-duplicate-urls.js <path-to-csv-file>
|
|
10
|
+
*/
|
|
11
|
+
function findDuplicateUrls(csvFilePath) {
|
|
12
|
+
// Validate command-line argument
|
|
13
|
+
if (!csvFilePath) {
|
|
14
|
+
console.error('Error: CSV file path is required');
|
|
15
|
+
console.error('Usage: node scripts/find-duplicate-urls.js <path-to-csv-file>');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate file exists and is readable
|
|
20
|
+
if (!fs.existsSync(csvFilePath)) {
|
|
21
|
+
console.error(`Error: File not found: ${csvFilePath}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const urlMap = new Map(); // url -> [memberId1, memberId2, ...]
|
|
26
|
+
let totalMembers = 0;
|
|
27
|
+
let rowNumber = 0;
|
|
28
|
+
let headersValidated = false;
|
|
29
|
+
let headers = null;
|
|
30
|
+
let urlColumnName = null;
|
|
31
|
+
let memberIdColumnName = null;
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
fs.createReadStream(csvFilePath)
|
|
35
|
+
.pipe(csv())
|
|
36
|
+
.on('headers', receivedHeaders => {
|
|
37
|
+
headers = receivedHeaders;
|
|
38
|
+
// Validate required columns exist - normalize by removing quotes, trimming, and lowercasing
|
|
39
|
+
const normalizedHeaders = headers.map(h => {
|
|
40
|
+
let normalized = String(h).trim();
|
|
41
|
+
// Remove all quotes (single and double) from the string
|
|
42
|
+
normalized = normalized.replace(/["']/g, '');
|
|
43
|
+
return normalized.toLowerCase().trim();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Find the actual column names for url and memberId
|
|
47
|
+
const urlIndex = normalizedHeaders.indexOf('url');
|
|
48
|
+
const memberIdIndex = normalizedHeaders.indexOf('memberid');
|
|
49
|
+
|
|
50
|
+
if (urlIndex === -1 || memberIdIndex === -1) {
|
|
51
|
+
console.error('Error: CSV must contain "url" and "memberId" columns (case-insensitive)');
|
|
52
|
+
console.error(`Found columns: ${headers.join(', ')}`);
|
|
53
|
+
console.error(`Normalized columns: ${normalizedHeaders.join(', ')}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Store the actual column names (with original casing/quotes)
|
|
58
|
+
urlColumnName = headers[urlIndex];
|
|
59
|
+
memberIdColumnName = headers[memberIdIndex];
|
|
60
|
+
headersValidated = true;
|
|
61
|
+
})
|
|
62
|
+
.on('data', row => {
|
|
63
|
+
// Validate headers on first data row if headers event didn't fire
|
|
64
|
+
if (!headersValidated) {
|
|
65
|
+
headers = Object.keys(row);
|
|
66
|
+
// Normalize by removing quotes, trimming, and lowercasing
|
|
67
|
+
const normalizedHeaders = headers.map(h => {
|
|
68
|
+
let normalized = String(h).trim();
|
|
69
|
+
// Remove all quotes (single and double) from the string
|
|
70
|
+
normalized = normalized.replace(/["']/g, '');
|
|
71
|
+
return normalized.toLowerCase().trim();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const urlIndex = normalizedHeaders.indexOf('url');
|
|
75
|
+
const memberIdIndex = normalizedHeaders.indexOf('memberid');
|
|
76
|
+
|
|
77
|
+
if (urlIndex === -1 || memberIdIndex === -1) {
|
|
78
|
+
console.error(
|
|
79
|
+
'Error: CSV must contain "url" and "memberId" columns (case-insensitive)'
|
|
80
|
+
);
|
|
81
|
+
console.error(`Found columns: ${headers.join(', ')}`);
|
|
82
|
+
console.error(`Normalized columns: ${normalizedHeaders.join(', ')}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Store the actual column names
|
|
87
|
+
urlColumnName = headers[urlIndex];
|
|
88
|
+
memberIdColumnName = headers[memberIdIndex];
|
|
89
|
+
headersValidated = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rowNumber++;
|
|
93
|
+
totalMembers++;
|
|
94
|
+
|
|
95
|
+
// Get URL and memberId using the actual column names from headers
|
|
96
|
+
const url = row[urlColumnName];
|
|
97
|
+
const memberId = row[memberIdColumnName];
|
|
98
|
+
|
|
99
|
+
// Skip rows with missing URL or memberId
|
|
100
|
+
if (!url || !memberId) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`Warning: Row ${rowNumber} skipped - missing url or memberId (url: ${url}, memberId: ${memberId})`
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const trimmedUrl = url.trim();
|
|
108
|
+
const trimmedMemberId = memberId.trim();
|
|
109
|
+
|
|
110
|
+
// Track URL occurrences
|
|
111
|
+
if (!urlMap.has(trimmedUrl)) {
|
|
112
|
+
urlMap.set(trimmedUrl, []);
|
|
113
|
+
}
|
|
114
|
+
urlMap.get(trimmedUrl).push(trimmedMemberId);
|
|
115
|
+
})
|
|
116
|
+
.on('error', error => {
|
|
117
|
+
console.error('Error reading CSV file:', error.message);
|
|
118
|
+
reject(error);
|
|
119
|
+
})
|
|
120
|
+
.on('end', () => {
|
|
121
|
+
if (!headersValidated) {
|
|
122
|
+
console.error('Error: Could not read CSV headers');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Find duplicates (URLs with count > 1)
|
|
127
|
+
const duplicateUrls = [];
|
|
128
|
+
let totalDuplicates = 0;
|
|
129
|
+
|
|
130
|
+
for (const [url, memberIds] of urlMap.entries()) {
|
|
131
|
+
if (memberIds.length > 1) {
|
|
132
|
+
duplicateUrls.push({
|
|
133
|
+
url: url,
|
|
134
|
+
count: memberIds.length,
|
|
135
|
+
memberIds: memberIds,
|
|
136
|
+
});
|
|
137
|
+
totalDuplicates += memberIds.length;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Sort by count (descending) then by URL (ascending)
|
|
142
|
+
duplicateUrls.sort((a, b) => {
|
|
143
|
+
if (b.count !== a.count) {
|
|
144
|
+
return b.count - a.count;
|
|
145
|
+
}
|
|
146
|
+
return a.url.localeCompare(b.url);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const totalUniqueUrls = urlMap.size;
|
|
150
|
+
const uniqueDuplicateUrls = duplicateUrls.length;
|
|
151
|
+
|
|
152
|
+
// Create a simple list of duplicated URLs (just the URL strings)
|
|
153
|
+
const duplicatedUrlsList = duplicateUrls.map(item => item.url);
|
|
154
|
+
|
|
155
|
+
// Generate report
|
|
156
|
+
const report = {
|
|
157
|
+
totalMembers: totalMembers,
|
|
158
|
+
totalUniqueUrls: totalUniqueUrls,
|
|
159
|
+
duplicateUrls: duplicateUrls,
|
|
160
|
+
duplicatedUrlsList: duplicatedUrlsList,
|
|
161
|
+
summary: {
|
|
162
|
+
totalDuplicates: totalDuplicates,
|
|
163
|
+
uniqueDuplicateUrls: uniqueDuplicateUrls,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Generate output filename
|
|
168
|
+
const csvDir = path.dirname(csvFilePath);
|
|
169
|
+
const csvBasename = path.basename(csvFilePath, path.extname(csvFilePath));
|
|
170
|
+
const outputPath = path.join(csvDir, `${csvBasename}-duplicate-urls-report.json`);
|
|
171
|
+
|
|
172
|
+
// Write JSON report
|
|
173
|
+
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf8');
|
|
174
|
+
|
|
175
|
+
console.log('\n=== Duplicate URL Report ===');
|
|
176
|
+
console.log(`Total members processed: ${totalMembers}`);
|
|
177
|
+
console.log(`Total unique URLs: ${totalUniqueUrls}`);
|
|
178
|
+
console.log(`Unique URLs with duplicates: ${uniqueDuplicateUrls}`);
|
|
179
|
+
console.log(`Total duplicate entries: ${totalDuplicates}`);
|
|
180
|
+
console.log(`\nReport saved to: ${outputPath}`);
|
|
181
|
+
console.log(`\nTop 10 most duplicated URLs:`);
|
|
182
|
+
duplicateUrls.slice(0, 10).forEach((item, index) => {
|
|
183
|
+
console.log(
|
|
184
|
+
` ${index + 1}. "${item.url}" - appears ${item.count} times (memberIds: ${item.memberIds.join(', ')})`
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
resolve(report);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Run if executed directly
|
|
194
|
+
if (require.main === module) {
|
|
195
|
+
const csvFilePath = process.argv[2];
|
|
196
|
+
findDuplicateUrls(csvFilePath).catch(error => {
|
|
197
|
+
console.error('Fatal error:', error.message);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { findDuplicateUrls };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abmp-npm",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.5",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"check-cycles": "madge --circular .",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"lint:fix": "eslint . --fix",
|
|
10
10
|
"format": "prettier --write \"**/*.{js,json,md}\"",
|
|
11
11
|
"format:check": "prettier --check \"**/*.{js,json,md}\"",
|
|
12
|
-
"prepare": "husky"
|
|
12
|
+
"prepare": "husky",
|
|
13
|
+
"find-duplicates": "node dev-only-scripts/find-duplicate-urls.js"
|
|
13
14
|
},
|
|
14
15
|
"author": "",
|
|
15
16
|
"license": "ISC",
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"crypto": "^1.0.1",
|
|
47
48
|
"jwt-js-decode": "^1.9.0",
|
|
48
49
|
"lodash": "^4.17.21",
|
|
50
|
+
"csv-parser": "^3.0.0",
|
|
49
51
|
"ngeohash": "^0.6.3",
|
|
50
52
|
"phone": "^3.1.67",
|
|
51
53
|
"psdev-task-manager": "1.1.7",
|
package/pages/Profile.js
CHANGED
|
@@ -52,7 +52,7 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
52
52
|
profileData.mainAddress
|
|
53
53
|
);
|
|
54
54
|
} else {
|
|
55
|
-
|
|
55
|
+
deleteElements(['#locationContainer', '#location1Container', '#locationContainer2']);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
setupAdditionalAddresses();
|
|
@@ -62,8 +62,8 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
62
62
|
_$w('#moreAdressesRepeater').data = profileData.processedAddresses;
|
|
63
63
|
|
|
64
64
|
if (profileData.processedAddresses.length > 0) {
|
|
65
|
-
_$w('#moreLocationButton').
|
|
66
|
-
_$w('#addressTitle').
|
|
65
|
+
_$w('#moreLocationButton').restore();
|
|
66
|
+
_$w('#addressTitle').delete();
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
_$w('#moreAdressesRepeater').onItemReady(($item, itemData) => {
|
|
@@ -81,9 +81,9 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
81
81
|
const $container = _$w(containerId);
|
|
82
82
|
|
|
83
83
|
$button.onClick(() => {
|
|
84
|
-
const
|
|
85
|
-
$container[
|
|
86
|
-
$button.label =
|
|
84
|
+
const isDeleted = $container.deleted;
|
|
85
|
+
$container[isDeleted ? 'restore' : 'delete']();
|
|
86
|
+
$button.label = isDeleted ? 'Less Locations -' : 'More Locations +';
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -98,15 +98,15 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
98
98
|
if (profileData.memberSince) {
|
|
99
99
|
_$w('#sinceYearText').text = profileData.memberSince;
|
|
100
100
|
} else {
|
|
101
|
-
_$w('#memberSinceBox').
|
|
101
|
+
_$w('#memberSinceBox').delete();
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function bindStudentBadge() {
|
|
106
106
|
if (profileData.shouldHaveStudentBadge) {
|
|
107
|
-
_$w('#studentContainer, #studentContainerMobile').
|
|
107
|
+
_$w('#studentContainer, #studentContainerMobile').restore();
|
|
108
108
|
} else {
|
|
109
|
-
_$w('#studentContainer, #studentContainerMobile').
|
|
109
|
+
_$w('#studentContainer, #studentContainerMobile').delete();
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -114,7 +114,7 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
114
114
|
if (profileData.logoImage) {
|
|
115
115
|
_$w('#logoImage').src = profileData.logoImage;
|
|
116
116
|
} else {
|
|
117
|
-
_$w('#logoImage').
|
|
117
|
+
_$w('#logoImage').delete();
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (profileData.profileImage) {
|
|
@@ -131,7 +131,7 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
131
131
|
profileData.fullName
|
|
132
132
|
);
|
|
133
133
|
} else {
|
|
134
|
-
|
|
134
|
+
deleteElements(['#fullNameText', '#fullNameText2', '#fullNameTextFoter']);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -149,7 +149,7 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
149
149
|
wixWindow.openLightbox(LIGHTBOX_NAMES.CONTACT_US, profileData)
|
|
150
150
|
);
|
|
151
151
|
} else {
|
|
152
|
-
_$w('#contactButton').
|
|
152
|
+
_$w('#contactButton').delete();
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -157,7 +157,7 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
157
157
|
if (profileData.bookingUrl) {
|
|
158
158
|
_$w('#bookNowButton').link = profileData.bookingUrl;
|
|
159
159
|
} else {
|
|
160
|
-
_$w('#bookNowButton').
|
|
160
|
+
_$w('#bookNowButton').delete();
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -172,7 +172,7 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
172
172
|
_$w('#phoneText').html = getPhoneHTML(_$w('#phoneText'));
|
|
173
173
|
_$w('#phoneText2').html = getPhoneHTML(_$w('#phoneText2'));
|
|
174
174
|
} else {
|
|
175
|
-
|
|
175
|
+
deleteElements(['#phoneContainer', '#phoneContainer2']);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -180,7 +180,7 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
180
180
|
if (profileData.licenceNo) {
|
|
181
181
|
_$w('#licenceNoText').text = profileData.licenceNo;
|
|
182
182
|
} else {
|
|
183
|
-
_$w('#licensesContainer').
|
|
183
|
+
_$w('#licensesContainer').delete();
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -194,16 +194,16 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
194
194
|
if (profileData.aboutService) {
|
|
195
195
|
_$w('#aboutYouText').html = profileData.aboutService;
|
|
196
196
|
} else {
|
|
197
|
-
_$w('#aboutSection').
|
|
197
|
+
_$w('#aboutSection').delete();
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
function bindBusinessName() {
|
|
202
202
|
if (profileData.businessName) {
|
|
203
203
|
_$w('#businessName').text = profileData.businessName;
|
|
204
|
-
_$w('#businessName').
|
|
204
|
+
_$w('#businessName').restore();
|
|
205
205
|
} else {
|
|
206
|
-
_$w('#businessName').
|
|
206
|
+
_$w('#businessName').delete();
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -213,13 +213,13 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
213
213
|
if (areasText) {
|
|
214
214
|
_$w('#areaOfPracticesText').text = areasText;
|
|
215
215
|
} else {
|
|
216
|
-
_$w('#areaOfPracticesText').
|
|
216
|
+
_$w('#areaOfPracticesText').delete();
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
if (Array.isArray(profileData.areasOfPractices) && profileData.areasOfPractices.length > 0) {
|
|
220
220
|
populateRepeater(profileData.areasOfPractices, '#areaOfPracticesRepeater', '#practiceText');
|
|
221
221
|
} else {
|
|
222
|
-
_$w('#servicesSection').
|
|
222
|
+
_$w('#servicesSection').delete();
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
@@ -229,16 +229,16 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
if (!profileData.gallery?.length) {
|
|
232
|
-
_$w('#gallerySection').
|
|
232
|
+
_$w('#gallerySection').delete();
|
|
233
233
|
} else {
|
|
234
234
|
_$w('#gallery').items = profileData.gallery;
|
|
235
|
-
_$w('#gallerySection').
|
|
235
|
+
_$w('#gallerySection').restore();
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
function bindTestimonialsData() {
|
|
240
240
|
if (!profileData.testimonials?.length) {
|
|
241
|
-
_$w('#testimonialsSection').
|
|
241
|
+
_$w('#testimonialsSection').delete();
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
|
|
@@ -265,9 +265,9 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
265
265
|
function setupTestimonialsIfAvailable() {
|
|
266
266
|
if (profileData.testimonials.length > 0) {
|
|
267
267
|
setupTestimonialsPagination(profileData.testimonials);
|
|
268
|
-
_$w('#testimonialsSection').
|
|
268
|
+
_$w('#testimonialsSection').restore();
|
|
269
269
|
} else {
|
|
270
|
-
_$w('#testimonialsSection').
|
|
270
|
+
_$w('#testimonialsSection').delete();
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
@@ -283,9 +283,9 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
283
283
|
});
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
function
|
|
286
|
+
function deleteElements(elementIds) {
|
|
287
287
|
elementIds.forEach(id => {
|
|
288
|
-
_$w(id).
|
|
288
|
+
_$w(id).delete();
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
291
|
|
|
@@ -332,15 +332,15 @@ async function profileOnReady({ $w: _$w }) {
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
function updateTestimonialNavigation(end, totalLength) {
|
|
335
|
-
_$w('#prevTestimonialBtn').
|
|
336
|
-
_$w('#nextTestimonialBtn').
|
|
335
|
+
_$w('#prevTestimonialBtn').delete();
|
|
336
|
+
_$w('#nextTestimonialBtn').delete();
|
|
337
337
|
|
|
338
338
|
if (currentTestimonialPage > 0) {
|
|
339
|
-
_$w('#prevTestimonialBtn').
|
|
339
|
+
_$w('#prevTestimonialBtn').restore();
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
if (end < totalLength) {
|
|
343
|
-
_$w('#nextTestimonialBtn').
|
|
343
|
+
_$w('#nextTestimonialBtn').restore();
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
}
|
package/pages/personalDetails.js
CHANGED
|
@@ -1106,6 +1106,7 @@ async function personalDetailsOnReady({
|
|
|
1106
1106
|
});
|
|
1107
1107
|
_$w('#profileLink').text = newProfileLink;
|
|
1108
1108
|
_$w('#profileLink').link = newProfileLink;
|
|
1109
|
+
_$w('#urlWebsiteText').text = newProfileLink;
|
|
1109
1110
|
|
|
1110
1111
|
_$w(SLUG_FLAGS.VALID).collapse();
|
|
1111
1112
|
_$w(SLUG_FLAGS.INVALID).collapse();
|
|
@@ -1858,8 +1859,15 @@ async function personalDetailsOnReady({
|
|
|
1858
1859
|
itemMemberObj.toShowPhone = null;
|
|
1859
1860
|
}
|
|
1860
1861
|
|
|
1862
|
+
if (itemMemberObj.phones) {
|
|
1863
|
+
itemMemberObj.phones = itemMemberObj.phones.filter(
|
|
1864
|
+
phone => phone !== phoneToRemove.phoneNumber
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1861
1868
|
const updatedData = currentData.filter(item => item._id !== phoneId);
|
|
1862
1869
|
renderPhonesList(updatedData);
|
|
1870
|
+
checkFormChanges(FORM_SECTION_HANDLER_MAP.CONTACT_BOOKING);
|
|
1863
1871
|
}
|
|
1864
1872
|
}
|
|
1865
1873
|
|
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
const { taskManager } = require('psdev-task-manager');
|
|
2
|
-
|
|
3
|
-
const { COLLECTIONS } = require('../../public/consts');
|
|
4
|
-
const { ensureUniqueUrl } = require('../daily-pull/process-member-methods');
|
|
5
|
-
const { wixData } = require('../elevated-modules');
|
|
6
|
-
// const { bulkSaveMembers } = require('../members-data-methods');
|
|
7
|
-
const { queryAllItems, chunkArray } = require('../utils');
|
|
8
|
-
|
|
9
|
-
const { TASKS_NAMES } = require('./consts');
|
|
10
|
-
|
|
11
|
-
const COLLECTION_WITH_URLS = 'MembersDataWithUrls';
|
|
12
|
-
const CHUNK_SIZE = 5000; // 5k members per task
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Step 1: Migrate existing URLs from backup collection
|
|
16
|
-
* Queries backup collection and schedules tasks with memberIds and URLs
|
|
17
|
-
*/
|
|
18
|
-
async function scheduleMigrateExistingUrls() {
|
|
19
|
-
console.log('=== Scheduling Step 1: Migrate Existing URLs ===');
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const membersQuery = await wixData.query(COLLECTION_WITH_URLS);
|
|
23
|
-
const startTime = Date.now();
|
|
24
|
-
const membersWithUrls = await queryAllItems(membersQuery);
|
|
25
|
-
const endTime = Date.now();
|
|
26
|
-
console.log(`QueryAllItems time: ${endTime - startTime}ms`);
|
|
27
|
-
|
|
28
|
-
const validMembers = membersWithUrls.filter(member => member.memberId && member.url);
|
|
29
|
-
console.log(`${validMembers.length} members have valid memberId and URL`);
|
|
30
|
-
|
|
31
|
-
if (validMembers.length === 0) {
|
|
32
|
-
console.log('No members to migrate URLs for');
|
|
33
|
-
return {
|
|
34
|
-
success: true,
|
|
35
|
-
message: 'No members need URL migration',
|
|
36
|
-
totalMembers: 0,
|
|
37
|
-
tasksScheduled: 0,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const migrationData = validMembers.map(member => ({
|
|
42
|
-
memberId: member.memberId,
|
|
43
|
-
url: member.url,
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
const chunks = chunkArray(migrationData, CHUNK_SIZE);
|
|
47
|
-
|
|
48
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
49
|
-
const chunk = chunks[i];
|
|
50
|
-
const task = {
|
|
51
|
-
name: TASKS_NAMES.migrateUrlsChunk,
|
|
52
|
-
data: {
|
|
53
|
-
urlData: chunk,
|
|
54
|
-
chunkIndex: i,
|
|
55
|
-
totalChunks: chunks.length,
|
|
56
|
-
},
|
|
57
|
-
type: 'scheduled',
|
|
58
|
-
};
|
|
59
|
-
await taskManager().schedule(task);
|
|
60
|
-
console.log(`Scheduled migration task ${i + 1}/${chunks.length} (${chunk.length} members)`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const result = {
|
|
64
|
-
success: true,
|
|
65
|
-
message: `Scheduled ${chunks.length} tasks for ${validMembers.length} members`,
|
|
66
|
-
totalMembers: validMembers.length,
|
|
67
|
-
tasksScheduled: chunks.length,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
console.log('=== Migration Scheduling Complete ===');
|
|
71
|
-
console.log(JSON.stringify(result, null, 2));
|
|
72
|
-
|
|
73
|
-
return result;
|
|
74
|
-
} catch (error) {
|
|
75
|
-
console.error('Error scheduling URL migration:', error);
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Process a chunk of URL migrations (called by task manager)
|
|
82
|
-
* Fetches members by memberId and updates with URLs using bulkSave
|
|
83
|
-
*/
|
|
84
|
-
async function migrateUrlsChunk(data) {
|
|
85
|
-
const { urlData, chunkIndex, totalChunks } = data;
|
|
86
|
-
console.log(
|
|
87
|
-
`Processing migration chunk ${chunkIndex + 1}/${totalChunks} (${urlData.length} members)`
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const result = {
|
|
91
|
-
successful: 0,
|
|
92
|
-
failed: 0,
|
|
93
|
-
skipped: 0,
|
|
94
|
-
errors: [],
|
|
95
|
-
skippedIds: [],
|
|
96
|
-
failedIds: [],
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const memberIds = urlData.map(({ memberId }) => memberId);
|
|
101
|
-
|
|
102
|
-
console.log(`Fetching ${memberIds.length} members from database...`);
|
|
103
|
-
const query = await wixData.query(COLLECTIONS.MEMBERS_DATA).hasSome('memberId', memberIds);
|
|
104
|
-
const members = await queryAllItems(query);
|
|
105
|
-
console.log(`Found ${members.length} members in database`);
|
|
106
|
-
|
|
107
|
-
const memberMap = new Map();
|
|
108
|
-
members.forEach(member => {
|
|
109
|
-
if (member.memberId) {
|
|
110
|
-
memberMap.set(member.memberId, member);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const membersToUpdate = [];
|
|
115
|
-
for (const { memberId, url } of urlData) {
|
|
116
|
-
const member = memberMap.get(memberId);
|
|
117
|
-
|
|
118
|
-
if (!member) {
|
|
119
|
-
console.log(`Member with memberId ${memberId} not found - skipping`);
|
|
120
|
-
result.skipped++;
|
|
121
|
-
result.skippedIds.push(memberId);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (member.url === url) {
|
|
126
|
-
console.log(`Member ${member._id} already has URL ${url} - skipping`);
|
|
127
|
-
result.skipped++;
|
|
128
|
-
result.skippedIds.push(memberId);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
membersToUpdate.push({
|
|
133
|
-
...member,
|
|
134
|
-
url: url,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (membersToUpdate.length === 0) {
|
|
139
|
-
console.log('No members need updating in this batch');
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
console.log(
|
|
144
|
-
`Started updating ${membersToUpdate.length} members with URLs in chunk ${chunkIndex}`
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
// keep bulk save as comment for now, since we are just testing the query and update logic
|
|
149
|
-
// await bulkSaveMembers(membersToUpdate);
|
|
150
|
-
result.successful += membersToUpdate.length;
|
|
151
|
-
console.log(`✅ Successfully updated ${membersToUpdate.length} members`);
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.error(`❌ Error bulk saving members:`, error);
|
|
154
|
-
result.failed += membersToUpdate.length;
|
|
155
|
-
// Add all member IDs to failedIds
|
|
156
|
-
result.failedIds.push(...membersToUpdate.map(m => m.memberId));
|
|
157
|
-
result.errors.push({
|
|
158
|
-
error: error.message,
|
|
159
|
-
memberCount: membersToUpdate.length,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
console.log(
|
|
164
|
-
`Chunk ${chunkIndex + 1} complete: ${result.successful} success, ${result.failed} failed, ${result.skipped} skipped`
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
// Log failed and skipped IDs if any
|
|
168
|
-
if (result.failedIds.length > 0) {
|
|
169
|
-
console.log(`❌ Failed memberIds (${result.failedIds.length}):`, result.failedIds);
|
|
170
|
-
}
|
|
171
|
-
if (result.skippedIds.length > 0) {
|
|
172
|
-
console.log(`⏭️ Skipped memberIds (${result.skippedIds.length}):`, result.skippedIds);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return result;
|
|
176
|
-
} catch (error) {
|
|
177
|
-
console.error(`Error processing migration chunk ${chunkIndex}:`, error);
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Step 2: Generate URLs for members without URLs
|
|
184
|
-
* Queries members without URLs and schedules generation tasks
|
|
185
|
-
*/
|
|
186
|
-
async function scheduleGenerateMissingUrls() {
|
|
187
|
-
console.log('=== Scheduling Step 2: Generate Missing URLs ===');
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const membersQuery = await wixData.query(COLLECTIONS.MEMBERS_DATA).isEmpty('url');
|
|
191
|
-
const membersToUpdate = await queryAllItems(membersQuery);
|
|
192
|
-
|
|
193
|
-
console.log(`Found ${membersToUpdate.length} members without URLs`);
|
|
194
|
-
|
|
195
|
-
if (membersToUpdate.length === 0) {
|
|
196
|
-
console.log('No members need URL generation');
|
|
197
|
-
return {
|
|
198
|
-
success: true,
|
|
199
|
-
message: 'No members need URL generation',
|
|
200
|
-
totalMembers: 0,
|
|
201
|
-
tasksScheduled: 0,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const chunks = chunkArray(membersToUpdate, CHUNK_SIZE);
|
|
206
|
-
|
|
207
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
208
|
-
const chunk = chunks[i];
|
|
209
|
-
const task = {
|
|
210
|
-
name: TASKS_NAMES.generateUrlsChunk,
|
|
211
|
-
data: {
|
|
212
|
-
memberIds: chunk.map(m => m._id),
|
|
213
|
-
chunkIndex: i,
|
|
214
|
-
totalChunks: chunks.length,
|
|
215
|
-
},
|
|
216
|
-
type: 'scheduled',
|
|
217
|
-
};
|
|
218
|
-
await taskManager().schedule(task);
|
|
219
|
-
console.log(`Scheduled generation task ${i + 1}/${chunks.length} (${chunk.length} members)`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const result = {
|
|
223
|
-
success: true,
|
|
224
|
-
message: `Scheduled ${chunks.length} tasks for ${membersToUpdate.length} members`,
|
|
225
|
-
totalMembers: membersToUpdate.length,
|
|
226
|
-
tasksScheduled: chunks.length,
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
console.log('=== Generation Scheduling Complete ===');
|
|
230
|
-
console.log(JSON.stringify(result, null, 2));
|
|
231
|
-
|
|
232
|
-
return result;
|
|
233
|
-
} catch (error) {
|
|
234
|
-
console.error('Error scheduling URL generation:', error);
|
|
235
|
-
throw error;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Process a chunk of URL generation (called by task manager)
|
|
241
|
-
* Fetches members, generates URLs, and bulk saves
|
|
242
|
-
*/
|
|
243
|
-
async function generateUrlsChunk(data) {
|
|
244
|
-
const { memberIds, chunkIndex, totalChunks } = data;
|
|
245
|
-
console.log(
|
|
246
|
-
`Processing generation chunk ${chunkIndex + 1}/${totalChunks} (${memberIds.length} members)`
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
const result = {
|
|
250
|
-
successful: 0,
|
|
251
|
-
failed: 0,
|
|
252
|
-
skipped: 0,
|
|
253
|
-
errors: [],
|
|
254
|
-
skippedIds: [],
|
|
255
|
-
failedIds: [],
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
// Fetch all members at once using hasSome
|
|
260
|
-
console.log(`Fetching ${memberIds.length} members from database...`);
|
|
261
|
-
const members = await queryAllItems(
|
|
262
|
-
wixData.query(COLLECTIONS.MEMBERS_DATA).hasSome('_id', memberIds)
|
|
263
|
-
);
|
|
264
|
-
console.log(`Found ${members.length} members in database`);
|
|
265
|
-
|
|
266
|
-
// Create a map of _id -> member for quick lookup
|
|
267
|
-
const memberMap = new Map();
|
|
268
|
-
members.forEach(member => {
|
|
269
|
-
memberMap.set(member._id, member);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// Process each member and generate URLs
|
|
273
|
-
const membersToUpdate = [];
|
|
274
|
-
for (const memberId of memberIds) {
|
|
275
|
-
const member = memberMap.get(memberId);
|
|
276
|
-
|
|
277
|
-
if (!member) {
|
|
278
|
-
console.log(`Member ${memberId} not found - skipping`);
|
|
279
|
-
result.skipped++;
|
|
280
|
-
result.skippedIds.push(memberId);
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (member.url) {
|
|
285
|
-
console.log(`Member ${memberId} already has URL - skipping`);
|
|
286
|
-
result.skipped++;
|
|
287
|
-
result.skippedIds.push(memberId);
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const name = member.fullName || `${member.firstName || ''} ${member.lastName || ''}`.trim();
|
|
292
|
-
|
|
293
|
-
if (!name) {
|
|
294
|
-
console.error(`Member ${memberId} has no name data - skipping`);
|
|
295
|
-
result.failed++;
|
|
296
|
-
result.failedIds.push(memberId);
|
|
297
|
-
result.errors.push({
|
|
298
|
-
memberId,
|
|
299
|
-
error: 'No name data available',
|
|
300
|
-
});
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
const uniqueUrl = await ensureUniqueUrl({
|
|
306
|
-
url: '',
|
|
307
|
-
memberId: member._id,
|
|
308
|
-
fullName: name,
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
console.log(`✅ Generated URL for member ${memberId}: ${uniqueUrl}`);
|
|
312
|
-
membersToUpdate.push({
|
|
313
|
-
...member,
|
|
314
|
-
url: uniqueUrl,
|
|
315
|
-
});
|
|
316
|
-
} catch (error) {
|
|
317
|
-
console.error(`❌ Failed to generate URL for member ${memberId}:`, error);
|
|
318
|
-
result.failed++;
|
|
319
|
-
result.failedIds.push(memberId);
|
|
320
|
-
result.errors.push({
|
|
321
|
-
memberId,
|
|
322
|
-
error: error.message || 'Unknown error',
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (membersToUpdate.length === 0) {
|
|
328
|
-
console.log('No members need updating in this batch');
|
|
329
|
-
return result;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
console.log(
|
|
333
|
-
`Started updating ${membersToUpdate.length} members with generated URLs in chunk ${chunkIndex}`
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
try {
|
|
337
|
-
// keep bulk save as comment for now, since we are just testing the query and update logic
|
|
338
|
-
// await bulkSaveMembers(membersToUpdate);
|
|
339
|
-
result.successful += membersToUpdate.length;
|
|
340
|
-
console.log(`✅ Successfully updated ${membersToUpdate.length} members`);
|
|
341
|
-
} catch (error) {
|
|
342
|
-
console.error(`❌ Error bulk saving members:`, error);
|
|
343
|
-
result.failed += membersToUpdate.length;
|
|
344
|
-
// Add all member _ids to failedIds
|
|
345
|
-
result.failedIds.push(...membersToUpdate.map(m => m._id));
|
|
346
|
-
result.errors.push({
|
|
347
|
-
error: error.message,
|
|
348
|
-
memberCount: membersToUpdate.length,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
console.log(
|
|
353
|
-
`Chunk ${chunkIndex + 1} complete: ${result.successful} success, ${result.failed} failed, ${result.skipped} skipped`
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
// Log failed and skipped IDs if any
|
|
357
|
-
if (result.failedIds.length > 0) {
|
|
358
|
-
console.log(`❌ Failed memberIds (${result.failedIds.length}):`, result.failedIds);
|
|
359
|
-
}
|
|
360
|
-
if (result.skippedIds.length > 0) {
|
|
361
|
-
console.log(`⏭️ Skipped memberIds (${result.skippedIds.length}):`, result.skippedIds);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return result;
|
|
365
|
-
} catch (error) {
|
|
366
|
-
console.error(`Error processing generation chunk ${chunkIndex}:`, error);
|
|
367
|
-
throw error;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
module.exports = {
|
|
372
|
-
scheduleMigrateExistingUrls,
|
|
373
|
-
migrateUrlsChunk,
|
|
374
|
-
scheduleGenerateMissingUrls,
|
|
375
|
-
generateUrlsChunk,
|
|
376
|
-
};
|