bulltrackers-module 1.0.592 → 1.0.593

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.
Files changed (36) hide show
  1. package/functions/old-generic-api/admin-api/index.js +895 -0
  2. package/functions/old-generic-api/helpers/api_helpers.js +457 -0
  3. package/functions/old-generic-api/index.js +204 -0
  4. package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +355 -0
  5. package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
  6. package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +212 -0
  7. package/functions/old-generic-api/user-api/helpers/collection_helpers.js +193 -0
  8. package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +68 -0
  9. package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +256 -0
  10. package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +640 -0
  11. package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +195 -0
  12. package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +503 -0
  13. package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
  14. package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +245 -0
  15. package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +174 -0
  16. package/functions/old-generic-api/user-api/helpers/data_helpers.js +87 -0
  17. package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +336 -0
  18. package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +615 -0
  19. package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +231 -0
  20. package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +641 -0
  21. package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +182 -0
  22. package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +137 -0
  23. package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +190 -0
  24. package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +66 -0
  25. package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +550 -0
  26. package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +378 -0
  27. package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +295 -0
  28. package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +162 -0
  29. package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +677 -0
  30. package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +323 -0
  31. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +96 -0
  32. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +141 -0
  33. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +310 -0
  34. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +829 -0
  35. package/functions/old-generic-api/user-api/index.js +109 -0
  36. package/package.json +2 -2
@@ -0,0 +1,640 @@
1
+ /**
2
+ * @fileoverview Path Resolution with Migration Support
3
+ * Enhanced path resolution using collection registry with auto-migration
4
+ * Supports both new CID-based paths and legacy paths with automatic migration
5
+ */
6
+
7
+ const { FieldValue } = require('@google-cloud/firestore');
8
+
9
+ // Try to require registry functions, but they may be injected instead
10
+ let getCollectionPath, resolvePath, getCollectionMetadata;
11
+ try {
12
+ const registry = require('../../../../../../config/collection_registry');
13
+ getCollectionPath = registry.getCollectionPath;
14
+ resolvePath = registry.resolvePath;
15
+ getCollectionMetadata = registry.getCollectionMetadata;
16
+ } catch (error) {
17
+ // Registry will be injected via dependencies
18
+ getCollectionPath = null;
19
+ resolvePath = null;
20
+ getCollectionMetadata = null;
21
+ }
22
+
23
+ /**
24
+ * Get registry functions from options or fallback to module-level
25
+ * @param {object} options - Options object that may contain collectionRegistry
26
+ * @returns {object} - Object with getCollectionPath, resolvePath, getCollectionMetadata
27
+ */
28
+ function getRegistryFunctions(options = {}) {
29
+ // Try to get from options first (injected)
30
+ if (options.collectionRegistry) {
31
+ // Try to get getCollectionMetadata from injected registry, fallback to module-level if not available
32
+ let getMetadata = options.collectionRegistry.getCollectionMetadata;
33
+ if (!getMetadata && getCollectionMetadata) {
34
+ getMetadata = getCollectionMetadata;
35
+ }
36
+ // If still not available, try requiring the registry
37
+ if (!getMetadata) {
38
+ try {
39
+ const registryFallback = require('../../../../../../config/collection_registry');
40
+ getMetadata = registryFallback.getCollectionMetadata;
41
+ } catch (e) {
42
+ // getMetadata will be undefined, which is fine - we'll handle it gracefully
43
+ }
44
+ }
45
+
46
+ return {
47
+ getCollectionPath: options.collectionRegistry.getCollectionPath,
48
+ resolvePath: options.collectionRegistry.resolvePath || resolvePath,
49
+ getCollectionMetadata: getMetadata
50
+ };
51
+ }
52
+
53
+ // Fallback to module-level (if required successfully)
54
+ if (getCollectionPath && resolvePath && getCollectionMetadata) {
55
+ return { getCollectionPath, resolvePath, getCollectionMetadata };
56
+ }
57
+
58
+ // Last resort: try to require again (in case it's available now)
59
+ try {
60
+ const registryLastResort = require('../../../../../../config/collection_registry');
61
+ return {
62
+ getCollectionPath: registryLastResort.getCollectionPath,
63
+ resolvePath: registryLastResort.resolvePath,
64
+ getCollectionMetadata: registryLastResort.getCollectionMetadata
65
+ };
66
+ } catch (error) {
67
+ // Return functions that might be available, with getCollectionMetadata as undefined
68
+ return {
69
+ getCollectionPath: getCollectionPath || null,
70
+ resolvePath: resolvePath || null,
71
+ getCollectionMetadata: getCollectionMetadata || null
72
+ };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get eToro CID from Firebase UID
78
+ * @param {object} db - Firestore instance
79
+ * @param {string} firebaseUid - Firebase authentication UID
80
+ * @returns {Promise<number|null>} - eToro CID or null if not found
81
+ */
82
+ async function getCidFromFirebaseUid(db, firebaseUid) {
83
+ try {
84
+ const userDoc = await db.collection('signedInUsers').doc(firebaseUid).get();
85
+ if (!userDoc.exists) return null;
86
+ const data = userDoc.data();
87
+ return data.etoroCID || data.cid || null;
88
+ } catch (error) {
89
+ console.error('[getCidFromFirebaseUid] Error fetching CID:', error);
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get new path from collection registry
96
+ * @param {string} category - Registry category (e.g., 'signedInUsers')
97
+ * @param {string} subcategory - Subcategory name (e.g., 'notifications')
98
+ * @param {object} params - Dynamic segment values (e.g., { cid: '123' })
99
+ * @param {object} options - Options object that may contain collectionRegistry
100
+ * @returns {string} - Resolved path
101
+ */
102
+ function getNewPath(category, subcategory, params = {}, options = {}) {
103
+ try {
104
+ const { getCollectionPath: getPath } = getRegistryFunctions(options);
105
+ return getPath(category, subcategory, params);
106
+ } catch (error) {
107
+ console.error(`[getNewPath] Error resolving path for ${category}/${subcategory}:`, error.message);
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get legacy path mapping for a collection type
114
+ * Maps new collection types to their legacy paths
115
+ * @param {string} dataType - Data type (e.g., 'notifications', 'alerts', 'watchlists')
116
+ * @param {string|number} userCid - User CID
117
+ * @param {object} config - Configuration object
118
+ * @returns {string|null} - Legacy path or null if no legacy path exists
119
+ */
120
+ /**
121
+ * Get legacy path mapping for a collection type
122
+ * Maps new collection types to their legacy paths based on user requirements
123
+ *
124
+ * This function first tries to get legacy paths from the collection registry,
125
+ * then falls back to the hardcoded legacyPathMap for backward compatibility.
126
+ *
127
+ * @param {string} dataType - Data type (e.g., 'notifications', 'alerts', 'watchlists')
128
+ * @param {string|number} userCid - User CID (or piCid for PI-specific data)
129
+ * @param {object} config - Configuration object
130
+ * @param {object} params - Additional parameters (e.g., { firebaseUid, username, date, requestId, etc. })
131
+ * @param {string} category - Registry category (e.g., 'signedInUsers', 'popularInvestors') - optional
132
+ * @param {string} subcategory - Registry subcategory (e.g., 'notifications', 'alerts') - optional
133
+ * @returns {string|null} - Legacy path template or null if no legacy path exists
134
+ */
135
+ function getLegacyPath(dataType, userCid, config = {}, params = {}, category = null, subcategory = null, options = {}) {
136
+ // Try to get legacy paths from collection registry first
137
+ if (category && subcategory) {
138
+ try {
139
+ const registryFuncs = getRegistryFunctions(options);
140
+ const getMetadata = registryFuncs.getCollectionMetadata;
141
+ const resolve = registryFuncs.resolvePath;
142
+
143
+ // Check if getCollectionMetadata is available - if not, fall through to hardcoded map
144
+ if (getMetadata && typeof getMetadata === 'function') {
145
+ const metadata = getMetadata(category, subcategory);
146
+ if (metadata && metadata.legacyPaths && metadata.legacyPaths.length > 0) {
147
+ // Use first legacy path from registry (can be enhanced to try multiple)
148
+ let legacyPathTemplate = metadata.legacyPaths[0];
149
+
150
+ // Resolve dynamic segments in the legacy path
151
+ const cid = String(userCid);
152
+ const resolvedParams = {
153
+ cid: cid,
154
+ userCid: cid,
155
+ firebaseUid: params.firebaseUid || '',
156
+ username: params.username || '',
157
+ date: params.date || '{date}',
158
+ requestId: params.requestId || '{requestId}',
159
+ piCid: params.piCid || cid,
160
+ reviewId: params.reviewId || `{piCid}_{userCid}`,
161
+ postId: params.postId || '{postId}',
162
+ viewId: params.viewId || `{piCid}_{viewerCid}_{timestamp}`,
163
+ watchlistId: params.watchlistId || '{watchlistId}',
164
+ version: params.version || '{version}',
165
+ ...params // Allow params to override defaults
166
+ };
167
+
168
+ // Resolve path template
169
+ legacyPathTemplate = resolve(legacyPathTemplate, resolvedParams);
170
+
171
+ // Replace any remaining placeholders
172
+ for (const [key, value] of Object.entries(resolvedParams)) {
173
+ legacyPathTemplate = legacyPathTemplate.replace(`{${key}}`, String(value));
174
+ }
175
+
176
+ return legacyPathTemplate;
177
+ }
178
+ // If metadata exists but no legacyPaths, fall through to hardcoded map
179
+ }
180
+ } catch (error) {
181
+ // Fall through to hardcoded map if registry lookup fails
182
+ // Only log if it's not the expected "not available" error
183
+ if (error.message !== 'getCollectionMetadata is not available') {
184
+ console.warn(`[getLegacyPath] Could not get legacy path from registry for ${category}/${subcategory}:`, error.message);
185
+ }
186
+ }
187
+ }
188
+
189
+ // Fallback to hardcoded legacy path map (for backward compatibility)
190
+ const cid = String(userCid);
191
+ const firebaseUid = params.firebaseUid || '';
192
+ const username = params.username || '';
193
+ const date = params.date || '{date}';
194
+ const requestId = params.requestId || '{requestId}';
195
+ const piCid = params.piCid || cid;
196
+ const reviewId = params.reviewId || `{piCid}_{userCid}`;
197
+ const postId = params.postId || '{postId}';
198
+ const viewId = params.viewId || `{piCid}_{viewerCid}_{timestamp}`;
199
+ const watchlistId = params.watchlistId || '{watchlistId}';
200
+ const version = params.version || '{version}';
201
+
202
+ const legacyPathMap = {
203
+ // Signed-in user data (CID-based)
204
+ notifications: firebaseUid ? `user_notifications/${firebaseUid}/notifications` : `user_notifications/${cid}/notifications`,
205
+ notificationsCounters: firebaseUid ? `user_notifications/${firebaseUid}/counters` : `user_notifications/${cid}/counters`,
206
+ alerts: `user_alerts/${cid}/alerts`,
207
+ alertsCounters: `user_alerts/${cid}/counters`,
208
+ watchlists: `user_watchlists/${cid}/lists`,
209
+ subscriptions: `watchlist_subscriptions/${cid}/alerts`,
210
+ verification: config.verificationsCollection ? `${config.verificationsCollection}/${username}` : `user_verifications/${username}`,
211
+ syncRequests: `user_sync_requests/${cid}/requests`,
212
+ syncStatus: `user_sync_requests/${cid}/global/latest`,
213
+ portfolio: config.signedInUsersCollection ? `${config.signedInUsersCollection}/19M/snapshots/${date}/parts` : `signed_in_users/19M/snapshots/${date}/parts`,
214
+ tradeHistory: config.signedInHistoryCollection ? `${config.signedInHistoryCollection}/19M/snapshots/${date}/parts` : `signed_in_user_history/19M/snapshots/${date}/parts`,
215
+ socialPosts: `signed_in_users_social/${cid}/posts`,
216
+
217
+ // Popular Investor data (PI CID-based)
218
+ piFetchRequests: `pi_fetch_requests/${piCid}/requests/${requestId}`,
219
+ piFetchStatus: `pi_fetch_requests/${piCid}/global/latest`,
220
+ piUserFetchRequests: `PopularInvestors/${piCid}/userFetchRequests/${cid}`,
221
+ piReviews: `pi_reviews/${reviewId}`,
222
+ piSocialPosts: `pi_social_posts/${piCid}/posts/${postId}`,
223
+ piProfileViews: `profile_views/${piCid}_${date}`,
224
+ piIndividualViews: `profile_views/individual_views/views/${viewId}`,
225
+
226
+ // Public/system data (no migration needed, but documented)
227
+ publicWatchlists: `public_watchlists/${watchlistId}/versions/${version}`,
228
+ userGcidMappings: `user_gcid_mappings/${cid}`,
229
+
230
+ // Root data collections (no migration - these are date-based and stay as-is)
231
+ // These are populated by task engines and should remain in their current format
232
+ signedInUserPortfolioRoot: `SignedInUserPortfolioData/${date}/${cid}`,
233
+ signedInUserTradeHistoryRoot: `SignedInUserTradeHistoryData/${date}/${cid}`,
234
+ signedInUserSocialRoot: `SignedInUserSocialPostData/${date}/${cid}`,
235
+ popularInvestorPortfolioRoot: `PopularInvestorPortfolioData/${date}/${piCid}`,
236
+ popularInvestorTradeHistoryRoot: `PopularInvestorTradeHistoryData/${date}/${piCid}`,
237
+ popularInvestorSocialRoot: `PopularInvestorSocialPostData/${date}/${piCid}`,
238
+ piPortfoliosDeep: `pi_portfolios_deep/19M/snapshots/${date}/parts`,
239
+ piPortfoliosOverall: `pi_portfolios_overall/19M/snapshots/${date}/parts`
240
+ };
241
+
242
+ return legacyPathMap[dataType] || null;
243
+ }
244
+
245
+ /**
246
+ * Read with migration - tries new path first, falls back to legacy, auto-migrates if found
247
+ * @param {object} db - Firestore instance
248
+ * @param {string} category - Registry category
249
+ * @param {string} subcategory - Subcategory name
250
+ * @param {object} params - Path parameters
251
+ * @param {object} options - Read options
252
+ * @param {boolean} options.isCollection - If true, reads as collection; if false, reads as document
253
+ * @param {string} options.dataType - Data type for legacy path lookup
254
+ * @param {object} options.config - Configuration object
255
+ * @param {object} options.logger - Logger instance
256
+ * @param {string} options.documentId - Document ID (for collections)
257
+ * @returns {Promise<object|null>} - Document data or snapshot, with migration info
258
+ */
259
+ async function readWithMigration(db, category, subcategory, params, options = {}) {
260
+ const {
261
+ isCollection = false,
262
+ dataType = null,
263
+ config = {},
264
+ logger = null,
265
+ documentId = null,
266
+ collectionRegistry = null
267
+ } = options;
268
+
269
+ // Add collectionRegistry to options for registry function access
270
+ const registryOptions = { collectionRegistry };
271
+
272
+ const userCid = params.cid || params.userCid;
273
+
274
+ // Get new path from registry
275
+ let newPath;
276
+ try {
277
+ newPath = getNewPath(category, subcategory, params, registryOptions);
278
+ } catch (error) {
279
+ if (logger) logger.log('WARN', `[readWithMigration] Could not resolve new path for ${category}/${subcategory}: ${error.message}`);
280
+ newPath = null;
281
+ }
282
+
283
+ // Try new path first
284
+ if (newPath) {
285
+ try {
286
+ if (isCollection) {
287
+ // Collection path must have odd number of segments
288
+ if (documentId) {
289
+ // Read specific document from collection
290
+ const docRef = db.collection(newPath).doc(documentId);
291
+ const doc = await docRef.get();
292
+ if (doc.exists) {
293
+ if (logger) logger.log('INFO', `[readWithMigration] Found document in new path: ${docRef.path}`);
294
+ return { data: doc.data(), exists: true, source: 'new', path: docRef.path };
295
+ }
296
+ } else {
297
+ // Read entire collection
298
+ const collectionRef = db.collection(newPath);
299
+ const snapshot = await collectionRef.get();
300
+ if (!snapshot.empty) {
301
+ if (logger) logger.log('INFO', `[readWithMigration] Found data in new path: ${newPath}`);
302
+ return { snapshot, source: 'new', path: newPath };
303
+ }
304
+ }
305
+ } else {
306
+ // Document path: if newPath has even segments, it's already a document path
307
+ // If documentId is provided, newPath should be collection path (odd segments)
308
+ let docRef;
309
+ if (documentId) {
310
+ // newPath should be collection (odd segments), add documentId
311
+ const pathSegments = newPath.split('/');
312
+ if (pathSegments.length % 2 === 0) {
313
+ // Even segments = document path, can't add documentId
314
+ throw new Error(`Path ${newPath} is a document path but documentId was provided`);
315
+ }
316
+ docRef = db.collection(newPath).doc(documentId);
317
+ } else {
318
+ // No documentId, newPath should be full document path (even segments)
319
+ docRef = db.doc(newPath);
320
+ }
321
+ const doc = await docRef.get();
322
+ if (doc.exists) {
323
+ if (logger) logger.log('INFO', `[readWithMigration] Found data in new path: ${docRef.path}`);
324
+ return { data: doc.data(), source: 'new', path: docRef.path };
325
+ }
326
+ }
327
+ } catch (newError) {
328
+ if (logger) logger.log('WARN', `[readWithMigration] Error reading from new path ${newPath}: ${newError.message}`);
329
+ }
330
+ }
331
+
332
+ // Fallback to legacy path
333
+ if (dataType && userCid) {
334
+ const legacyPathTemplate = getLegacyPath(dataType, userCid, config, params, category, subcategory, registryOptions);
335
+ if (legacyPathTemplate) {
336
+ try {
337
+ // Resolve legacy path (may have additional params like date, username)
338
+ // Replace placeholders in legacy path template
339
+ let legacyPath = legacyPathTemplate;
340
+ for (const [key, value] of Object.entries(params)) {
341
+ legacyPath = legacyPath.replace(`{${key}}`, String(value));
342
+ }
343
+ // Also replace common placeholders
344
+ legacyPath = legacyPath.replace(/{date}/g, params.date || '{date}');
345
+ legacyPath = legacyPath.replace(/{requestId}/g, params.requestId || documentId || '{requestId}');
346
+ legacyPath = legacyPath.replace(/{username}/g, params.username || '{username}');
347
+ legacyPath = legacyPath.replace(/{piCid}/g, params.piCid || userCid);
348
+ legacyPath = legacyPath.replace(/{userCid}/g, String(userCid));
349
+ legacyPath = legacyPath.replace(/{cid}/g, String(userCid));
350
+ legacyPath = legacyPath.replace(/{viewerCid}/g, params.viewerCid || '{viewerCid}');
351
+ legacyPath = legacyPath.replace(/{timestamp}/g, params.timestamp || Date.now().toString());
352
+ legacyPath = legacyPath.replace(/{viewId}/g, params.viewId || documentId || '{viewId}');
353
+ legacyPath = legacyPath.replace(/{reviewId}/g, params.reviewId || documentId || `{piCid}_{userCid}`);
354
+ legacyPath = legacyPath.replace(/{postId}/g, params.postId || documentId || '{postId}');
355
+ legacyPath = legacyPath.replace(/{watchlistId}/g, params.watchlistId || '{watchlistId}');
356
+ legacyPath = legacyPath.replace(/{version}/g, params.version || '{version}');
357
+
358
+ if (isCollection) {
359
+ if (documentId) {
360
+ // Read specific document from legacy collection
361
+ const legacyDocRef = db.collection(legacyPath).doc(documentId);
362
+ const legacyDoc = await legacyDocRef.get();
363
+ if (legacyDoc.exists) {
364
+ if (logger) logger.log('INFO', `[readWithMigration] Found document in legacy path: ${legacyDocRef.path}, will migrate`);
365
+
366
+ // Auto-migrate if new path is available
367
+ if (newPath) {
368
+ await migrateDocumentData(db, legacyDoc, newPath, documentId, logger);
369
+ }
370
+
371
+ return { data: legacyDoc.data(), exists: true, source: 'legacy', path: legacyDocRef.path, migrated: !!newPath };
372
+ }
373
+ } else {
374
+ // Read entire legacy collection
375
+ const legacyCollectionRef = db.collection(legacyPath);
376
+ const legacySnapshot = await legacyCollectionRef.get();
377
+ if (!legacySnapshot.empty) {
378
+ if (logger) logger.log('INFO', `[readWithMigration] Found data in legacy path: ${legacyPath}, will migrate`);
379
+
380
+ // Auto-migrate if new path is available
381
+ if (newPath) {
382
+ await migrateCollectionData(db, legacySnapshot, newPath, dataType, logger);
383
+ }
384
+
385
+ return { snapshot: legacySnapshot, source: 'legacy', path: legacyPath, migrated: !!newPath };
386
+ }
387
+ }
388
+ } else {
389
+ const legacyDocRef = documentId
390
+ ? db.collection(legacyPath).doc(documentId)
391
+ : db.doc(legacyPath);
392
+ const legacyDoc = await legacyDocRef.get();
393
+ if (legacyDoc.exists) {
394
+ if (logger) logger.log('INFO', `[readWithMigration] Found data in legacy path: ${legacyDocRef.path}, will migrate`);
395
+
396
+ // Auto-migrate if new path is available
397
+ if (newPath) {
398
+ await migrateDocumentData(db, legacyDoc, newPath, documentId, logger);
399
+ }
400
+
401
+ return { data: legacyDoc.data(), source: 'legacy', path: legacyDocRef.path, migrated: !!newPath };
402
+ }
403
+ }
404
+ } catch (legacyError) {
405
+ if (logger) logger.log('WARN', `[readWithMigration] Error reading from legacy path: ${legacyError.message}`);
406
+ }
407
+ }
408
+ }
409
+
410
+ return null;
411
+ }
412
+
413
+ /**
414
+ * Migrate document data from legacy to new path
415
+ * @param {object} db - Firestore instance
416
+ * @param {object} legacyDoc - Legacy document snapshot
417
+ * @param {string} newPath - New path (collection path, not full document path)
418
+ * @param {string} documentId - Document ID
419
+ * @param {object} logger - Logger instance
420
+ * @returns {Promise<void>}
421
+ */
422
+ async function migrateDocumentData(db, legacyDoc, newPath, documentId, logger) {
423
+ try {
424
+ const data = legacyDoc.data();
425
+ const newDocRef = documentId
426
+ ? db.collection(newPath).doc(documentId)
427
+ : db.doc(newPath);
428
+
429
+ // Check if already migrated
430
+ const existingDoc = await newDocRef.get();
431
+ if (existingDoc.exists) {
432
+ if (logger) logger.log('INFO', `[migrateDocumentData] Data already exists in new path: ${newDocRef.path}`);
433
+ return;
434
+ }
435
+
436
+ // Migrate data
437
+ await newDocRef.set({
438
+ ...data,
439
+ _migratedAt: FieldValue.serverTimestamp(),
440
+ _migratedFrom: legacyDoc.ref.path
441
+ });
442
+
443
+ if (logger) logger.log('SUCCESS', `[migrateDocumentData] Migrated document from ${legacyDoc.ref.path} to ${newDocRef.path}`);
444
+ } catch (error) {
445
+ if (logger) logger.log('ERROR', `[migrateDocumentData] Migration failed: ${error.message}`);
446
+ // Don't throw - migration failures shouldn't break reads
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Migrate collection data from legacy to new path
452
+ * @param {object} db - Firestore instance
453
+ * @param {object} legacySnapshot - Legacy collection snapshot
454
+ * @param {string} newPath - New collection path
455
+ * @param {string} dataType - Data type
456
+ * @param {object} logger - Logger instance
457
+ * @returns {Promise<void>}
458
+ */
459
+ async function migrateCollectionData(db, legacySnapshot, newPath, dataType, logger) {
460
+ try {
461
+ const batch = db.batch();
462
+ let migratedCount = 0;
463
+
464
+ for (const doc of legacySnapshot.docs) {
465
+ const newDocRef = db.collection(newPath).doc(doc.id);
466
+
467
+ // Check if already migrated
468
+ const existingDoc = await newDocRef.get();
469
+ if (existingDoc.exists) {
470
+ continue; // Skip if already migrated
471
+ }
472
+
473
+ // Add to batch
474
+ batch.set(newDocRef, {
475
+ ...doc.data(),
476
+ _migratedAt: FieldValue.serverTimestamp(),
477
+ _migratedFrom: doc.ref.path
478
+ });
479
+ migratedCount++;
480
+ }
481
+
482
+ if (migratedCount > 0) {
483
+ await batch.commit();
484
+ if (logger) logger.log('SUCCESS', `[migrateCollectionData] Migrated ${migratedCount} documents from legacy to ${newPath}`);
485
+ }
486
+ } catch (error) {
487
+ if (logger) logger.log('ERROR', `[migrateCollectionData] Migration failed: ${error.message}`);
488
+ // Don't throw - migration failures shouldn't break reads
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Write with dual write support (writes to both new and legacy during migration)
494
+ * @param {object} db - Firestore instance
495
+ * @param {string} category - Registry category
496
+ * @param {string} subcategory - Subcategory name
497
+ * @param {object} params - Path parameters
498
+ * @param {object} data - Data to write
499
+ * @param {object} options - Write options
500
+ * @param {boolean} options.isCollection - If true, writes as collection; if false, writes as document
501
+ * @param {boolean} options.merge - If true, merges data instead of overwriting
502
+ * @param {string} options.dataType - Data type for legacy path lookup
503
+ * @param {object} options.config - Configuration object
504
+ * @param {string} options.documentId - Document ID (required for collections)
505
+ * @param {boolean} options.dualWrite - If true, writes to both paths (default: true during migration)
506
+ * @returns {Promise<void>}
507
+ */
508
+ async function writeWithMigration(db, category, subcategory, params, data, options = {}) {
509
+ const {
510
+ isCollection = false,
511
+ merge = false,
512
+ dataType = null,
513
+ config = {},
514
+ documentId = null,
515
+ dualWrite = true, // Default to dual write during migration period
516
+ collectionRegistry = null
517
+ } = options;
518
+
519
+ // Add collectionRegistry to options for registry function access
520
+ const registryOptions = { collectionRegistry };
521
+
522
+ const userCid = params.cid || params.userCid;
523
+
524
+ // Get new path
525
+ const newPath = getNewPath(category, subcategory, params, registryOptions);
526
+
527
+ // Get legacy path if dual write is enabled
528
+ let legacyPath = null;
529
+ if (dualWrite && dataType && userCid) {
530
+ try {
531
+ const registryFuncs = getRegistryFunctions(registryOptions);
532
+ const resolve = registryFuncs?.resolvePath;
533
+ legacyPath = getLegacyPath(dataType, userCid, config, params, null, null, registryOptions);
534
+ if (legacyPath && resolve && typeof resolve === 'function') {
535
+ legacyPath = resolve(legacyPath, params);
536
+ } else if (legacyPath) {
537
+ // If resolve is not available, manually replace placeholders
538
+ for (const [key, value] of Object.entries(params)) {
539
+ legacyPath = legacyPath.replace(`{${key}}`, String(value));
540
+ }
541
+ }
542
+ } catch (error) {
543
+ console.warn('[writeWithMigration] Could not resolve legacy path, skipping dual write:', error.message);
544
+ legacyPath = null; // Skip dual write if we can't resolve legacy path
545
+ }
546
+ }
547
+
548
+ const batch = db.batch();
549
+
550
+ try {
551
+ // Write to new path
552
+ if (isCollection) {
553
+ if (!documentId) {
554
+ throw new Error('Collection writes require documentId');
555
+ }
556
+ // Collection path must have odd number of segments
557
+ const pathSegments = newPath.split('/');
558
+ if (pathSegments.length % 2 === 0) {
559
+ throw new Error(`Path ${newPath} is a document path but isCollection=true was specified`);
560
+ }
561
+ const newRef = db.collection(newPath).doc(documentId);
562
+ if (merge) {
563
+ batch.set(newRef, data, { merge: true });
564
+ } else {
565
+ batch.set(newRef, data);
566
+ }
567
+ } else {
568
+ // Document path: if documentId provided, newPath should be collection (odd segments)
569
+ // If no documentId, newPath should be full document path (even segments)
570
+ let newRef;
571
+ if (documentId) {
572
+ const pathSegments = newPath.split('/');
573
+ if (pathSegments.length % 2 === 0) {
574
+ throw new Error(`Path ${newPath} is a document path but documentId was provided`);
575
+ }
576
+ newRef = db.collection(newPath).doc(documentId);
577
+ } else {
578
+ newRef = db.doc(newPath);
579
+ }
580
+ if (merge) {
581
+ batch.set(newRef, data, { merge: true });
582
+ } else {
583
+ batch.set(newRef, data);
584
+ }
585
+ }
586
+
587
+ // Write to legacy path if dual write is enabled
588
+ if (legacyPath) {
589
+ if (isCollection) {
590
+ if (!documentId) {
591
+ throw new Error('Collection writes require documentId');
592
+ }
593
+ const legacyRef = db.collection(legacyPath).doc(documentId);
594
+ if (merge) {
595
+ batch.set(legacyRef, data, { merge: true });
596
+ } else {
597
+ batch.set(legacyRef, data);
598
+ }
599
+ } else {
600
+ const legacyRef = documentId
601
+ ? db.collection(legacyPath).doc(documentId)
602
+ : db.doc(legacyPath);
603
+ if (merge) {
604
+ batch.set(legacyRef, data, { merge: true });
605
+ } else {
606
+ batch.set(legacyRef, data);
607
+ }
608
+ }
609
+ }
610
+
611
+ await batch.commit();
612
+ } catch (error) {
613
+ console.error('[writeWithMigration] Error writing data:', error);
614
+ throw error;
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Get collection path helper (for backward compatibility)
620
+ * @param {object} collectionRegistry - Collection registry (injected)
621
+ * @param {string} category - Registry category
622
+ * @param {string} subcategory - Subcategory name
623
+ * @param {object} params - Path parameters
624
+ * @returns {string} - Resolved path
625
+ */
626
+ function getCollectionPathHelper(collectionRegistry, category, subcategory, params = {}) {
627
+ return getNewPath(category, subcategory, params, { collectionRegistry });
628
+ }
629
+
630
+ module.exports = {
631
+ getCidFromFirebaseUid,
632
+ getNewPath,
633
+ getLegacyPath,
634
+ readWithMigration,
635
+ writeWithMigration,
636
+ migrateDocumentData,
637
+ migrateCollectionData,
638
+ getCollectionPath: getCollectionPathHelper // For backward compatibility
639
+ };
640
+