abmp-npm 1.1.85 → 1.1.86

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.
@@ -300,4 +300,5 @@ async function createCoreMemberData(inputMemberData, existingDbMember, currentPa
300
300
 
301
301
  module.exports = {
302
302
  generateUpdatedMemberData,
303
+ ensureUniqueUrl,
303
304
  };
package/backend/index.js CHANGED
@@ -14,4 +14,6 @@ module.exports = {
14
14
  ...require('./data-hooks'),
15
15
  ...require('./http-functions'),
16
16
  ...require('./dev-only-methods'),
17
+ ...require('./tasks/migration-methods'),
18
+ ...require('./tasks/url-migration-methods'),
17
19
  };
@@ -12,6 +12,10 @@ 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',
15
19
  };
16
20
 
17
21
  module.exports = {
@@ -3,4 +3,5 @@ module.exports = {
3
3
  ...require('./consts'),
4
4
  ...require('./tasks-process-methods'),
5
5
  ...require('./migration-methods'),
6
+ ...require('./url-migration-methods'),
6
7
  };
@@ -20,7 +20,27 @@ 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
+
23
41
  module.exports = {
24
42
  scheduleConvertHtmlToRichContent,
25
43
  scheduleExternalProfileImageMigration,
44
+ scheduleUrlMigration,
45
+ scheduleUrlGeneration,
26
46
  };
@@ -17,6 +17,12 @@ 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');
20
26
 
21
27
  const getDailyMembersDataSyncChildTasks = () => {
22
28
  // 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
@@ -89,7 +95,7 @@ const TASKS = {
89
95
  getIdentifier: () => 'SHOULD_NEVER_SKIP',
90
96
  process: updateSiteMapS3,
91
97
  shouldSkipCheck: () => false,
92
- estimatedDurationSec: 90,
98
+ estimatedDurationSec: 70,
93
99
  },
94
100
  [TASKS_NAMES.scheduleContactFormEmailMigration]: {
95
101
  name: TASKS_NAMES.scheduleContactFormEmailMigration,
@@ -119,6 +125,34 @@ const TASKS = {
119
125
  shouldSkipCheck: () => false,
120
126
  estimatedDurationSec: 45,
121
127
  },
128
+ [TASKS_NAMES.scheduleMigrateExistingUrls]: {
129
+ name: TASKS_NAMES.scheduleMigrateExistingUrls,
130
+ getIdentifier: () => 'SHOULD_NEVER_SKIP',
131
+ process: scheduleMigrateExistingUrls,
132
+ shouldSkipCheck: () => false,
133
+ estimatedDurationSec: 80,
134
+ },
135
+ [TASKS_NAMES.migrateUrlsChunk]: {
136
+ name: TASKS_NAMES.migrateUrlsChunk,
137
+ getIdentifier: task => task.data,
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: 80,
148
+ },
149
+ [TASKS_NAMES.generateUrlsChunk]: {
150
+ name: TASKS_NAMES.generateUrlsChunk,
151
+ getIdentifier: task => task.data,
152
+ process: generateUrlsChunk,
153
+ shouldSkipCheck: () => false,
154
+ estimatedDurationSec: 80,
155
+ },
122
156
  };
123
157
 
124
158
  module.exports = { TASKS };
@@ -0,0 +1,363 @@
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
+ await bulkSaveMembers(membersToUpdate);
149
+ result.successful += membersToUpdate.length;
150
+ console.log(`✅ Successfully updated ${membersToUpdate.length} members`);
151
+ } catch (error) {
152
+ console.error(`❌ Error bulk saving members:`, error);
153
+ result.failed += membersToUpdate.length;
154
+ // Add all member IDs to failedIds
155
+ result.failedIds.push(...membersToUpdate.map(m => m.memberId));
156
+ result.errors.push({
157
+ error: error.message,
158
+ memberCount: membersToUpdate.length,
159
+ });
160
+ }
161
+
162
+ console.log(
163
+ `Chunk ${chunkIndex + 1} complete: ${result.successful} success, ${result.failed} failed, ${result.skipped} skipped`
164
+ );
165
+
166
+ // Log failed and skipped IDs if any
167
+ if (result.failedIds.length > 0) {
168
+ console.log(`❌ Failed memberIds (${result.failedIds.length}):`, result.failedIds);
169
+ }
170
+ if (result.skippedIds.length > 0) {
171
+ console.log(`⏭️ Skipped memberIds (${result.skippedIds.length}):`, result.skippedIds);
172
+ }
173
+
174
+ return result;
175
+ } catch (error) {
176
+ console.error(`Error processing migration chunk ${chunkIndex}:`, error);
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Step 2: Generate URLs for members without URLs
183
+ * Queries members without URLs and schedules generation tasks
184
+ */
185
+ async function scheduleGenerateMissingUrls() {
186
+ console.log('=== Scheduling Step 2: Generate Missing URLs ===');
187
+
188
+ try {
189
+ const membersQuery = await wixData.query(COLLECTIONS.MEMBERS_DATA).isEmpty('url');
190
+ const membersToUpdate = await queryAllItems(membersQuery);
191
+
192
+ console.log(`Found ${membersToUpdate.length} members without URLs`);
193
+
194
+ if (membersToUpdate.length === 0) {
195
+ console.log('No members need URL generation');
196
+ return {
197
+ success: true,
198
+ message: 'No members need URL generation',
199
+ totalMembers: 0,
200
+ tasksScheduled: 0,
201
+ };
202
+ }
203
+
204
+ const chunks = chunkArray(membersToUpdate, CHUNK_SIZE);
205
+
206
+ for (let i = 0; i < chunks.length; i++) {
207
+ const chunk = chunks[i];
208
+ const task = {
209
+ name: TASKS_NAMES.generateUrlsChunk,
210
+ data: {
211
+ memberIds: chunk.map(m => m._id),
212
+ chunkIndex: i,
213
+ totalChunks: chunks.length,
214
+ },
215
+ type: 'scheduled',
216
+ };
217
+ await taskManager().schedule(task);
218
+ console.log(`Scheduled generation task ${i + 1}/${chunks.length} (${chunk.length} members)`);
219
+ }
220
+
221
+ const result = {
222
+ success: true,
223
+ message: `Scheduled ${chunks.length} tasks for ${membersToUpdate.length} members`,
224
+ totalMembers: membersToUpdate.length,
225
+ tasksScheduled: chunks.length,
226
+ };
227
+
228
+ console.log('=== Generation Scheduling Complete ===');
229
+ console.log(JSON.stringify(result, null, 2));
230
+
231
+ return result;
232
+ } catch (error) {
233
+ console.error('Error scheduling URL generation:', error);
234
+ throw error;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Process a chunk of URL generation (called by task manager)
240
+ * Fetches members, generates URLs, and bulk saves
241
+ */
242
+ async function generateUrlsChunk(data) {
243
+ const { memberIds, chunkIndex, totalChunks } = data;
244
+ console.log(
245
+ `Processing generation chunk ${chunkIndex + 1}/${totalChunks} (${memberIds.length} members)`
246
+ );
247
+
248
+ const result = {
249
+ successful: 0,
250
+ failed: 0,
251
+ skipped: 0,
252
+ errors: [],
253
+ skippedIds: [],
254
+ failedIds: [],
255
+ };
256
+
257
+ try {
258
+ // Fetch all members at once using hasSome
259
+ console.log(`Fetching ${memberIds.length} members from database...`);
260
+ const members = await queryAllItems(
261
+ wixData.query(COLLECTIONS.MEMBERS_DATA).hasSome('_id', memberIds)
262
+ );
263
+ console.log(`Found ${members.length} members in database`);
264
+
265
+ // Create a map of _id -> member for quick lookup
266
+ const memberMap = new Map();
267
+ members.forEach(member => {
268
+ memberMap.set(member._id, member);
269
+ });
270
+
271
+ // Process each member and generate URLs
272
+ const membersToUpdate = [];
273
+ for (const memberId of memberIds) {
274
+ const member = memberMap.get(memberId);
275
+
276
+ if (!member) {
277
+ console.log(`Member ${memberId} not found - skipping`);
278
+ result.skipped++;
279
+ result.skippedIds.push(memberId);
280
+ continue;
281
+ }
282
+
283
+ if (member.url) {
284
+ console.log(`Member ${memberId} already has URL - skipping`);
285
+ result.skipped++;
286
+ result.skippedIds.push(memberId);
287
+ continue;
288
+ }
289
+
290
+ const name = member.fullName || `${member.firstName || ''} ${member.lastName || ''}`.trim();
291
+
292
+ try {
293
+ const uniqueUrl = await ensureUniqueUrl({
294
+ url: '',
295
+ memberId: member._id,
296
+ fullName: name || '', // Let ensureUniqueUrl handle fallback for empty names
297
+ });
298
+
299
+ console.log(`✅ Generated URL for member ${memberId}: ${uniqueUrl}`);
300
+ membersToUpdate.push({
301
+ ...member,
302
+ url: uniqueUrl,
303
+ });
304
+ } catch (error) {
305
+ console.error(`❌ Failed to generate URL for member ${memberId}:`, error);
306
+ result.failed++;
307
+ result.failedIds.push(memberId);
308
+ result.errors.push({
309
+ memberId,
310
+ error: error.message || 'Unknown error',
311
+ });
312
+ }
313
+ }
314
+
315
+ if (membersToUpdate.length === 0) {
316
+ console.log('No members need updating in this batch');
317
+ return result;
318
+ }
319
+
320
+ console.log(
321
+ `Started updating ${membersToUpdate.length} members with generated URLs in chunk ${chunkIndex}`
322
+ );
323
+
324
+ try {
325
+ await bulkSaveMembers(membersToUpdate);
326
+ result.successful += membersToUpdate.length;
327
+ console.log(`✅ Successfully updated ${membersToUpdate.length} members`);
328
+ } catch (error) {
329
+ console.error(`❌ Error bulk saving members:`, error);
330
+ result.failed += membersToUpdate.length;
331
+ // Add all member _ids to failedIds
332
+ result.failedIds.push(...membersToUpdate.map(m => m._id));
333
+ result.errors.push({
334
+ error: error.message,
335
+ memberCount: membersToUpdate.length,
336
+ });
337
+ }
338
+
339
+ console.log(
340
+ `Chunk ${chunkIndex + 1} complete: ${result.successful} success, ${result.failed} failed, ${result.skipped} skipped`
341
+ );
342
+
343
+ // Log failed and skipped IDs if any
344
+ if (result.failedIds.length > 0) {
345
+ console.log(`❌ Failed memberIds (${result.failedIds.length}):`, result.failedIds);
346
+ }
347
+ if (result.skippedIds.length > 0) {
348
+ console.log(`⏭️ Skipped memberIds (${result.skippedIds.length}):`, result.skippedIds);
349
+ }
350
+
351
+ return result;
352
+ } catch (error) {
353
+ console.error(`Error processing generation chunk ${chunkIndex}:`, error);
354
+ throw error;
355
+ }
356
+ }
357
+
358
+ module.exports = {
359
+ scheduleMigrateExistingUrls,
360
+ migrateUrlsChunk,
361
+ scheduleGenerateMissingUrls,
362
+ generateUrlsChunk,
363
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmp-npm",
3
- "version": "1.1.85",
3
+ "version": "1.1.86",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "check-cycles": "madge --circular .",
@@ -1294,6 +1294,7 @@ async function personalDetailsOnReady({
1294
1294
  streetAddress: {
1295
1295
  name: extractStreetName(address.line1),
1296
1296
  number: extractStreetNumber(address.line1),
1297
+ apt: address.line2 || '',
1297
1298
  },
1298
1299
  city: address.city || '',
1299
1300
  subdivision: address.state || '',
@@ -1309,20 +1310,49 @@ async function personalDetailsOnReady({
1309
1310
  if (!addressInputValue) return null;
1310
1311
 
1311
1312
  let line1 = '';
1313
+ let line2 = '';
1314
+
1312
1315
  if (addressInputValue.streetAddress) {
1313
1316
  const number = addressInputValue.streetAddress.number || '';
1314
1317
  const name = addressInputValue.streetAddress.name || '';
1315
1318
  line1 = `${number} ${name}`.trim();
1319
+
1320
+ // Capture apartment/suite/building info from streetAddress.apt (undocumented but exists)
1321
+ if (addressInputValue.streetAddress.apt) {
1322
+ line2 = addressInputValue.streetAddress.apt;
1323
+ }
1316
1324
  }
1317
1325
 
1318
1326
  if (!line1 && addressInputValue.formatted) {
1319
1327
  line1 = addressInputValue.formatted.split(',')[0]?.trim() || '';
1320
1328
  }
1321
1329
 
1330
+ // If line2 is still empty, try to extract building/suite info from formatted address
1331
+ if (!line2 && addressInputValue.formatted) {
1332
+ const formattedParts = addressInputValue.formatted.split(',').map(part => part.trim());
1333
+ // Look for BLDG/STE/APT/UNIT/SUITE info in the formatted parts
1334
+ for (let i = 1; i < formattedParts.length; i++) {
1335
+ const part = formattedParts[i];
1336
+ const lowerPart = part.toLowerCase();
1337
+ if (
1338
+ lowerPart.includes('bldg') ||
1339
+ lowerPart.includes('ste') ||
1340
+ lowerPart.includes('apt') ||
1341
+ lowerPart.includes('unit') ||
1342
+ lowerPart.includes('suite') ||
1343
+ lowerPart.includes('#') ||
1344
+ lowerPart.includes('building')
1345
+ ) {
1346
+ line2 = part;
1347
+ break;
1348
+ }
1349
+ }
1350
+ }
1351
+
1322
1352
  return {
1323
1353
  key: existingAddress?.key || generateId(),
1324
1354
  line1,
1325
- line2: existingAddress?.line2 || '',
1355
+ line2: line2 || existingAddress?.line2 || '',
1326
1356
  city: addressInputValue.city || '',
1327
1357
  state: addressInputValue.subdivision || '',
1328
1358
  postalcode: addressInputValue.postalCode || '',
@@ -1705,6 +1735,7 @@ async function personalDetailsOnReady({
1705
1735
  const parts = [];
1706
1736
 
1707
1737
  if (addr.line1) parts.push(addr.line1);
1738
+ if (addr.line2) parts.push(addr.line2); // Include building/suite info
1708
1739
  if (addr.city) parts.push(addr.city);
1709
1740
  if (addr.state && addr.postalcode) {
1710
1741
  parts.push(`${addr.state} ${addr.postalcode}`);