abmp-npm 2.0.56 → 2.0.59
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__/ensure-unique-urls-in-batch.test.js +129 -0
- package/backend/daily-pull/bulk-process-methods.js +3 -4
- package/backend/jobs.js +15 -0
- package/backend/tasks/consts.js +2 -0
- package/backend/tasks/index.js +1 -0
- package/backend/tasks/tasks-configs.js +15 -0
- package/backend/tasks/url-space-fix-methods.js +174 -0
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const { ensureUniqueUrlsInBatch } = require('../daily-pull/bulk-process-methods');
|
|
2
|
+
const membersDataMethods = require('../members-data-methods');
|
|
3
|
+
jest.mock('../members-data-methods');
|
|
4
|
+
|
|
5
|
+
describe('ensureUniqueUrlsInBatch', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
membersDataMethods.getMemberBySlug.mockReset();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('case-insensitivity (regression: avoid creating John-12 AND john-12)', () => {
|
|
11
|
+
test('merges John-11 and john-11 into one group and assigns unique sequential URLs', async () => {
|
|
12
|
+
membersDataMethods.getMemberBySlug.mockResolvedValue({ url: 'John-11', memberId: 1 });
|
|
13
|
+
|
|
14
|
+
const members = [
|
|
15
|
+
{ memberId: 101, url: 'John-11', fullName: 'John Doe' },
|
|
16
|
+
{ memberId: 102, url: 'john-11', fullName: 'John Doe' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const result = await ensureUniqueUrlsInBatch(members);
|
|
20
|
+
|
|
21
|
+
const urls = result.map(m => m.url);
|
|
22
|
+
|
|
23
|
+
expect(urls).toHaveLength(2);
|
|
24
|
+
expect(new Set(urls).size).toBe(2);
|
|
25
|
+
expect(urls.some(u => u.endsWith('-12'))).toBe(true);
|
|
26
|
+
expect(urls.some(u => u.endsWith('-13'))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('does NOT create both John-12 and john-12 (case duplicate)', async () => {
|
|
30
|
+
membersDataMethods.getMemberBySlug.mockResolvedValue({ url: 'John-11', memberId: 1 });
|
|
31
|
+
|
|
32
|
+
const members = [
|
|
33
|
+
{ memberId: 101, url: 'John-11' },
|
|
34
|
+
{ memberId: 102, url: 'john-11' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const result = await ensureUniqueUrlsInBatch(members);
|
|
38
|
+
const urls = result.map(m => m.url);
|
|
39
|
+
|
|
40
|
+
const hasJohn12 = urls.some(u => u === 'John-12');
|
|
41
|
+
const hasjohn12 = urls.some(u => u === 'john-12');
|
|
42
|
+
expect(hasJohn12 && hasjohn12).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('handles JOHN, John, john variants - all merge into single group', async () => {
|
|
46
|
+
membersDataMethods.getMemberBySlug.mockResolvedValue(null);
|
|
47
|
+
|
|
48
|
+
const members = [
|
|
49
|
+
{ memberId: 101, url: 'JOHN-5' },
|
|
50
|
+
{ memberId: 102, url: 'John-5' },
|
|
51
|
+
{ memberId: 103, url: 'john-5' },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const result = await ensureUniqueUrlsInBatch(members);
|
|
55
|
+
const urls = result.map(m => m.url);
|
|
56
|
+
|
|
57
|
+
expect(urls).toHaveLength(3);
|
|
58
|
+
expect(new Set(urls).size).toBe(3);
|
|
59
|
+
const bases = urls.map(u => u.replace(/-\d+$/, '').toLowerCase());
|
|
60
|
+
expect(new Set(bases).size).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('multiple members with same base URL', () => {
|
|
65
|
+
test('assigns sequential counters starting after batch max and DB max', async () => {
|
|
66
|
+
membersDataMethods.getMemberBySlug.mockResolvedValue({
|
|
67
|
+
url: 'firstNameLastName-11',
|
|
68
|
+
memberId: 1,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const members = [
|
|
72
|
+
{ memberId: 201, url: 'firstNameLastName-11' },
|
|
73
|
+
{ memberId: 202, url: 'firstNameLastName-11' },
|
|
74
|
+
{ memberId: 203, url: 'firstNameLastName-11' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const result = await ensureUniqueUrlsInBatch(members);
|
|
78
|
+
const urls = result.map(m => m.url).sort();
|
|
79
|
+
|
|
80
|
+
expect(urls).toEqual([
|
|
81
|
+
'firstNameLastName-12',
|
|
82
|
+
'firstNameLastName-13',
|
|
83
|
+
'firstNameLastName-14',
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('uses batch max when DB has no matches', async () => {
|
|
88
|
+
membersDataMethods.getMemberBySlug.mockResolvedValue(null);
|
|
89
|
+
|
|
90
|
+
const members = [
|
|
91
|
+
{ memberId: 301, url: 'testUser-5' },
|
|
92
|
+
{ memberId: 302, url: 'testUser-5' },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const result = await ensureUniqueUrlsInBatch(members);
|
|
96
|
+
const urls = result.map(m => m.url).sort();
|
|
97
|
+
|
|
98
|
+
expect(urls).toEqual(['testUser-6', 'testUser-7']);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('edge cases', () => {
|
|
103
|
+
test('returns empty array unchanged', async () => {
|
|
104
|
+
const result = await ensureUniqueUrlsInBatch([]);
|
|
105
|
+
expect(result).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('returns non-array unchanged', async () => {
|
|
109
|
+
const input = { not: 'array' };
|
|
110
|
+
const result = await ensureUniqueUrlsInBatch(input);
|
|
111
|
+
expect(result).toBe(input);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('skips members without url', async () => {
|
|
115
|
+
membersDataMethods.getMemberBySlug.mockResolvedValue(null);
|
|
116
|
+
|
|
117
|
+
const members = [
|
|
118
|
+
{ memberId: 401, url: 'validUrl' },
|
|
119
|
+
{ memberId: 402, url: null },
|
|
120
|
+
{ memberId: 403 },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const result = await ensureUniqueUrlsInBatch(members);
|
|
124
|
+
expect(result).toHaveLength(3);
|
|
125
|
+
expect(result[0].url).toBe('validUrl');
|
|
126
|
+
expect(result[1].url).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -28,14 +28,13 @@ async function ensureUniqueUrlsInBatch(memberDataList) {
|
|
|
28
28
|
const baseUrl = extractBaseUrl(member.url);
|
|
29
29
|
const groupKey = baseUrl.toLowerCase();
|
|
30
30
|
if (!urlGroups.has(groupKey)) {
|
|
31
|
-
urlGroups.set(groupKey, []);
|
|
31
|
+
urlGroups.set(groupKey, { members: [], baseUrl });
|
|
32
32
|
}
|
|
33
|
-
urlGroups.get(groupKey).push(member);
|
|
33
|
+
urlGroups.get(groupKey).members.push(member);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
// For each group, check database and assign unique URLs sequentially
|
|
37
|
-
for (const [
|
|
38
|
-
const baseUrl = groupKey; // lowercase for consistent slug assignment
|
|
37
|
+
for (const [, { members, baseUrl }] of urlGroups.entries()) {
|
|
39
38
|
if (members.length <= 1) {
|
|
40
39
|
// Single member - still check DB to ensure it doesn't conflict with other pages
|
|
41
40
|
const member = members[0];
|
package/backend/jobs.js
CHANGED
|
@@ -82,6 +82,20 @@ async function scheduleFixPrimaryAddressForMembersTask() {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
async function scheduleFixUrlsWithSpacesTask() {
|
|
86
|
+
try {
|
|
87
|
+
console.log('scheduleFixUrlsWithSpaces started!');
|
|
88
|
+
return await taskManager().schedule({
|
|
89
|
+
name: TASKS_NAMES.scheduleFixUrlsWithSpaces,
|
|
90
|
+
data: {},
|
|
91
|
+
type: 'scheduled',
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`Failed to scheduleFixUrlsWithSpaces: ${error.message}`);
|
|
95
|
+
throw new Error(`Failed to scheduleFixUrlsWithSpaces: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
async function updateSiteMapS3() {
|
|
86
100
|
try {
|
|
87
101
|
return await taskManager().schedule({
|
|
@@ -100,4 +114,5 @@ module.exports = {
|
|
|
100
114
|
updateSiteMapS3,
|
|
101
115
|
scheduleCreateContactsFromMembersTask,
|
|
102
116
|
scheduleFixPrimaryAddressForMembersTask,
|
|
117
|
+
scheduleFixUrlsWithSpacesTask,
|
|
103
118
|
};
|
package/backend/tasks/consts.js
CHANGED
|
@@ -20,6 +20,8 @@ const TASKS_NAMES = {
|
|
|
20
20
|
createContactsFromMembers: 'createContactsFromMembers',
|
|
21
21
|
scheduleFixPrimaryAddressForMembers: 'scheduleFixPrimaryAddressForMembers',
|
|
22
22
|
fixPrimaryAddressChunk: 'fixPrimaryAddressChunk',
|
|
23
|
+
scheduleFixUrlsWithSpaces: 'scheduleFixUrlsWithSpaces',
|
|
24
|
+
fixUrlsWithSpacesChunk: 'fixUrlsWithSpacesChunk',
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
module.exports = {
|
package/backend/tasks/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
scheduleGenerateMissingUrls,
|
|
30
30
|
generateUrlsChunk,
|
|
31
31
|
} = require('./url-migration-methods');
|
|
32
|
+
const { scheduleFixUrlsWithSpaces, fixUrlsWithSpacesChunk } = require('./url-space-fix-methods');
|
|
32
33
|
|
|
33
34
|
const getDailyMembersDataSyncChildTasks = () => {
|
|
34
35
|
// 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
|
|
@@ -187,6 +188,20 @@ const TASKS = {
|
|
|
187
188
|
shouldSkipCheck: () => false,
|
|
188
189
|
estimatedDurationSec: 80,
|
|
189
190
|
},
|
|
191
|
+
[TASKS_NAMES.scheduleFixUrlsWithSpaces]: {
|
|
192
|
+
name: TASKS_NAMES.scheduleFixUrlsWithSpaces,
|
|
193
|
+
getIdentifier: () => 'SHOULD_NEVER_SKIP',
|
|
194
|
+
process: scheduleFixUrlsWithSpaces,
|
|
195
|
+
shouldSkipCheck: () => false,
|
|
196
|
+
estimatedDurationSec: 80,
|
|
197
|
+
},
|
|
198
|
+
[TASKS_NAMES.fixUrlsWithSpacesChunk]: {
|
|
199
|
+
name: TASKS_NAMES.fixUrlsWithSpacesChunk,
|
|
200
|
+
getIdentifier: task => task.data,
|
|
201
|
+
process: fixUrlsWithSpacesChunk,
|
|
202
|
+
shouldSkipCheck: () => false,
|
|
203
|
+
estimatedDurationSec: 80,
|
|
204
|
+
},
|
|
190
205
|
};
|
|
191
206
|
|
|
192
207
|
module.exports = { TASKS };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const { taskManager } = require('psdev-task-manager');
|
|
2
|
+
|
|
3
|
+
const { COLLECTIONS } = require('../../public/consts');
|
|
4
|
+
const { ensureUniqueUrlsInBatch } = require('../daily-pull/bulk-process-methods');
|
|
5
|
+
const { ensureUniqueUrl } = require('../daily-pull/process-member-methods');
|
|
6
|
+
const { wixData } = require('../elevated-modules');
|
|
7
|
+
const { bulkSaveMembers, getMembersByIds } = require('../members-data-methods');
|
|
8
|
+
const { queryAllItems, chunkArray } = require('../utils');
|
|
9
|
+
|
|
10
|
+
const { TASKS_NAMES } = require('./consts');
|
|
11
|
+
|
|
12
|
+
const CHUNK_SIZE = 1000;
|
|
13
|
+
|
|
14
|
+
const hasSpace = value => typeof value === 'string' && /\s/.test(value);
|
|
15
|
+
|
|
16
|
+
const normalizeSlug = value => (value || '').replace(/\s+/g, '').trim();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Schedules tasks to fix member URLs that contain spaces.
|
|
20
|
+
*/
|
|
21
|
+
async function scheduleFixUrlsWithSpaces() {
|
|
22
|
+
console.log('=== Scheduling Fix URLs With Spaces ===');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const membersQuery = await wixData
|
|
26
|
+
.query(COLLECTIONS.MEMBERS_DATA)
|
|
27
|
+
.contains('url', ' ')
|
|
28
|
+
.limit(1000);
|
|
29
|
+
const members = await queryAllItems(membersQuery);
|
|
30
|
+
console.log(`Fetched ${members.length} members with spaces in url`);
|
|
31
|
+
|
|
32
|
+
const memberIds = [
|
|
33
|
+
...new Set(
|
|
34
|
+
members
|
|
35
|
+
.map(member => Number(member.memberId))
|
|
36
|
+
.filter(memberId => Number.isFinite(memberId) && memberId > 0)
|
|
37
|
+
),
|
|
38
|
+
];
|
|
39
|
+
console.log(`Members to fix: ${memberIds.length}`);
|
|
40
|
+
|
|
41
|
+
if (memberIds.length === 0) {
|
|
42
|
+
console.log('No members need URL space fixes');
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
message: 'No members need URL space fixes',
|
|
46
|
+
totalMembers: 0,
|
|
47
|
+
tasksScheduled: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const chunks = chunkArray(memberIds, CHUNK_SIZE);
|
|
52
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
53
|
+
const chunk = chunks[i];
|
|
54
|
+
const task = {
|
|
55
|
+
name: TASKS_NAMES.fixUrlsWithSpacesChunk,
|
|
56
|
+
data: {
|
|
57
|
+
memberIds: chunk,
|
|
58
|
+
chunkIndex: i,
|
|
59
|
+
totalChunks: chunks.length,
|
|
60
|
+
},
|
|
61
|
+
type: 'scheduled',
|
|
62
|
+
};
|
|
63
|
+
await taskManager().schedule(task);
|
|
64
|
+
console.log(`Scheduled task ${i + 1}/${chunks.length} (${chunk.length} members)`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = {
|
|
68
|
+
success: true,
|
|
69
|
+
message: `Scheduled ${chunks.length} tasks for ${memberIds.length} members`,
|
|
70
|
+
totalMembers: memberIds.length,
|
|
71
|
+
tasksScheduled: chunks.length,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
console.log('=== Scheduling Complete ===');
|
|
75
|
+
console.log(`Sample memberIds: ${memberIds.slice(0, 10).join(', ')}`);
|
|
76
|
+
console.log(JSON.stringify(result, null, 2));
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error scheduling URL space fixes:', error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Processes a chunk of members and fixes URLs with spaces.
|
|
87
|
+
*/
|
|
88
|
+
async function fixUrlsWithSpacesChunk(data) {
|
|
89
|
+
const { memberIds, chunkIndex, totalChunks } = data;
|
|
90
|
+
console.log(
|
|
91
|
+
`Processing URL space fix chunk ${chunkIndex + 1}/${totalChunks} (${memberIds.length} members)`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const result = {
|
|
95
|
+
successful: 0,
|
|
96
|
+
failed: 0,
|
|
97
|
+
skipped: 0,
|
|
98
|
+
errors: [],
|
|
99
|
+
skippedIds: [],
|
|
100
|
+
failedIds: [],
|
|
101
|
+
};
|
|
102
|
+
const updatedIds = [];
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const members = await getMembersByIds(memberIds);
|
|
106
|
+
console.log(`Loaded ${members.length} members for this chunk`);
|
|
107
|
+
|
|
108
|
+
const membersToUpdate = [];
|
|
109
|
+
for (const member of members) {
|
|
110
|
+
if (!hasSpace(member.url)) {
|
|
111
|
+
result.skipped++;
|
|
112
|
+
result.skippedIds.push(member.memberId);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const normalized = normalizeSlug(member.url);
|
|
117
|
+
const ensuredUrl = await ensureUniqueUrl({
|
|
118
|
+
url: normalized,
|
|
119
|
+
memberId: member.memberId,
|
|
120
|
+
fullName: member.fullName,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!ensuredUrl || ensuredUrl === member.url) {
|
|
124
|
+
result.skipped++;
|
|
125
|
+
result.skippedIds.push(member.memberId);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
membersToUpdate.push({
|
|
130
|
+
...member,
|
|
131
|
+
url: ensuredUrl,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (membersToUpdate.length === 0) {
|
|
136
|
+
console.log('No members need updating in this batch');
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const uniqueUpdates = await ensureUniqueUrlsInBatch(membersToUpdate);
|
|
141
|
+
uniqueUpdates.forEach(member => {
|
|
142
|
+
if (updatedIds.length < 20) {
|
|
143
|
+
updatedIds.push(member.memberId);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await bulkSaveMembers(uniqueUpdates);
|
|
149
|
+
result.successful += uniqueUpdates.length;
|
|
150
|
+
console.log(`✅ Successfully updated ${uniqueUpdates.length} members`);
|
|
151
|
+
if (updatedIds.length > 0) {
|
|
152
|
+
console.log(`Updated memberIds (sample): ${updatedIds.join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('❌ Error bulk saving members:', error);
|
|
156
|
+
result.failed += uniqueUpdates.length;
|
|
157
|
+
result.failedIds.push(...uniqueUpdates.map(member => member.memberId));
|
|
158
|
+
result.errors.push({
|
|
159
|
+
error: error.message,
|
|
160
|
+
memberCount: uniqueUpdates.length,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(`Error processing URL space fix chunk ${chunkIndex}:`, error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
scheduleFixUrlsWithSpaces,
|
|
173
|
+
fixUrlsWithSpacesChunk,
|
|
174
|
+
};
|