bulltrackers-module 1.0.504 → 1.0.506
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/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +345 -0
- package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +320 -0
- package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +116 -0
- package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +171 -0
- package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +710 -0
- package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +109 -0
- package/functions/generic-api/user-api/MIGRATION_PLAN.md +499 -0
- package/functions/generic-api/user-api/README_MIGRATION.md +152 -0
- package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +106 -0
- package/functions/generic-api/user-api/REFACTORING_STATUS.md +85 -0
- package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +206 -0
- package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +126 -0
- package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
- package/functions/generic-api/user-api/helpers/{test_alert_helpers.js → alerts/test_alert_helpers.js} +1 -1
- package/functions/generic-api/user-api/helpers/collection_helpers.js +23 -45
- package/functions/generic-api/user-api/helpers/core/compression_helpers.js +68 -0
- package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +213 -0
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +486 -0
- package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +77 -0
- package/functions/generic-api/user-api/helpers/data/computation_helpers.js +299 -0
- package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
- package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +238 -0
- package/functions/generic-api/user-api/helpers/data/social_helpers.js +55 -0
- package/functions/generic-api/user-api/helpers/data_helpers.js +85 -2750
- package/functions/generic-api/user-api/helpers/{dev_helpers.js → dev/dev_helpers.js} +0 -1
- package/functions/generic-api/user-api/helpers/{on_demand_fetch_helpers.js → fetch/on_demand_fetch_helpers.js} +33 -115
- package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +360 -0
- package/functions/generic-api/user-api/helpers/{notification_helpers.js → notifications/notification_helpers.js} +0 -1
- package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +200 -0
- package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +125 -0
- package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +178 -0
- package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +65 -0
- package/functions/generic-api/user-api/helpers/{review_helpers.js → reviews/review_helpers.js} +23 -107
- package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +177 -0
- package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +70 -0
- package/functions/generic-api/user-api/helpers/{user_sync_helpers.js → sync/user_sync_helpers.js} +54 -127
- package/functions/generic-api/user-api/helpers/{verification_helpers.js → verification/verification_helpers.js} +4 -43
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +95 -0
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +139 -0
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +306 -0
- package/functions/generic-api/user-api/helpers/{watchlist_helpers.js → watchlist/watchlist_management_helpers.js} +62 -213
- package/functions/generic-api/user-api/index.js +9 -9
- package/functions/task-engine/handler_creator.js +7 -6
- package/package.json +1 -1
- package/functions/generic-api/API_MIGRATION_PLAN.md +0 -436
- package/functions/generic-api/user-api/helpers/FALLBACK_CONDITIONS.md +0 -98
- package/functions/generic-api/user-api/helpers/HISTORY_STORAGE_LOCATION.md +0 -66
- package/functions/generic-api/user-api/helpers/subscription_helpers.js +0 -512
- /package/functions/generic-api/user-api/helpers/{alert_helpers.js → alerts/alert_helpers.js} +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
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
|
+
const { getCollectionPath, resolvePath, getCollectionMetadata } = require('../../../../../../config/collection_registry');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get eToro CID from Firebase UID
|
|
12
|
+
* @param {object} db - Firestore instance
|
|
13
|
+
* @param {string} firebaseUid - Firebase authentication UID
|
|
14
|
+
* @returns {Promise<number|null>} - eToro CID or null if not found
|
|
15
|
+
*/
|
|
16
|
+
async function getCidFromFirebaseUid(db, firebaseUid) {
|
|
17
|
+
try {
|
|
18
|
+
const userDoc = await db.collection('signedInUsers').doc(firebaseUid).get();
|
|
19
|
+
if (!userDoc.exists) return null;
|
|
20
|
+
const data = userDoc.data();
|
|
21
|
+
return data.etoroCID || data.cid || null;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('[getCidFromFirebaseUid] Error fetching CID:', error);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get new path from collection registry
|
|
30
|
+
* @param {string} category - Registry category (e.g., 'signedInUsers')
|
|
31
|
+
* @param {string} subcategory - Subcategory name (e.g., 'notifications')
|
|
32
|
+
* @param {object} params - Dynamic segment values (e.g., { cid: '123' })
|
|
33
|
+
* @returns {string} - Resolved path
|
|
34
|
+
*/
|
|
35
|
+
function getNewPath(category, subcategory, params = {}) {
|
|
36
|
+
try {
|
|
37
|
+
return getCollectionPath(category, subcategory, params);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`[getNewPath] Error resolving path for ${category}/${subcategory}:`, error.message);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get legacy path mapping for a collection type
|
|
46
|
+
* Maps new collection types to their legacy paths
|
|
47
|
+
* @param {string} dataType - Data type (e.g., 'notifications', 'alerts', 'watchlists')
|
|
48
|
+
* @param {string|number} userCid - User CID
|
|
49
|
+
* @param {object} config - Configuration object
|
|
50
|
+
* @returns {string|null} - Legacy path or null if no legacy path exists
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Get legacy path mapping for a collection type
|
|
54
|
+
* Maps new collection types to their legacy paths based on user requirements
|
|
55
|
+
*
|
|
56
|
+
* This function first tries to get legacy paths from the collection registry,
|
|
57
|
+
* then falls back to the hardcoded legacyPathMap for backward compatibility.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} dataType - Data type (e.g., 'notifications', 'alerts', 'watchlists')
|
|
60
|
+
* @param {string|number} userCid - User CID (or piCid for PI-specific data)
|
|
61
|
+
* @param {object} config - Configuration object
|
|
62
|
+
* @param {object} params - Additional parameters (e.g., { firebaseUid, username, date, requestId, etc. })
|
|
63
|
+
* @param {string} category - Registry category (e.g., 'signedInUsers', 'popularInvestors') - optional
|
|
64
|
+
* @param {string} subcategory - Registry subcategory (e.g., 'notifications', 'alerts') - optional
|
|
65
|
+
* @returns {string|null} - Legacy path template or null if no legacy path exists
|
|
66
|
+
*/
|
|
67
|
+
function getLegacyPath(dataType, userCid, config = {}, params = {}, category = null, subcategory = null) {
|
|
68
|
+
// Try to get legacy paths from collection registry first
|
|
69
|
+
if (category && subcategory) {
|
|
70
|
+
try {
|
|
71
|
+
const metadata = getCollectionMetadata(category, subcategory);
|
|
72
|
+
if (metadata && metadata.legacyPaths && metadata.legacyPaths.length > 0) {
|
|
73
|
+
// Use first legacy path from registry (can be enhanced to try multiple)
|
|
74
|
+
let legacyPathTemplate = metadata.legacyPaths[0];
|
|
75
|
+
|
|
76
|
+
// Resolve dynamic segments in the legacy path
|
|
77
|
+
const cid = String(userCid);
|
|
78
|
+
const resolvedParams = {
|
|
79
|
+
cid: cid,
|
|
80
|
+
userCid: cid,
|
|
81
|
+
firebaseUid: params.firebaseUid || '',
|
|
82
|
+
username: params.username || '',
|
|
83
|
+
date: params.date || '{date}',
|
|
84
|
+
requestId: params.requestId || '{requestId}',
|
|
85
|
+
piCid: params.piCid || cid,
|
|
86
|
+
reviewId: params.reviewId || `{piCid}_{userCid}`,
|
|
87
|
+
postId: params.postId || '{postId}',
|
|
88
|
+
viewId: params.viewId || `{piCid}_{viewerCid}_{timestamp}`,
|
|
89
|
+
watchlistId: params.watchlistId || '{watchlistId}',
|
|
90
|
+
version: params.version || '{version}',
|
|
91
|
+
...params // Allow params to override defaults
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Resolve path template
|
|
95
|
+
legacyPathTemplate = resolvePath(legacyPathTemplate, resolvedParams);
|
|
96
|
+
|
|
97
|
+
// Replace any remaining placeholders
|
|
98
|
+
for (const [key, value] of Object.entries(resolvedParams)) {
|
|
99
|
+
legacyPathTemplate = legacyPathTemplate.replace(`{${key}}`, String(value));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return legacyPathTemplate;
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Fall through to hardcoded map if registry lookup fails
|
|
106
|
+
console.warn(`[getLegacyPath] Could not get legacy path from registry for ${category}/${subcategory}:`, error.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fallback to hardcoded legacy path map (for backward compatibility)
|
|
111
|
+
const cid = String(userCid);
|
|
112
|
+
const firebaseUid = params.firebaseUid || '';
|
|
113
|
+
const username = params.username || '';
|
|
114
|
+
const date = params.date || '{date}';
|
|
115
|
+
const requestId = params.requestId || '{requestId}';
|
|
116
|
+
const piCid = params.piCid || cid;
|
|
117
|
+
const reviewId = params.reviewId || `{piCid}_{userCid}`;
|
|
118
|
+
const postId = params.postId || '{postId}';
|
|
119
|
+
const viewId = params.viewId || `{piCid}_{viewerCid}_{timestamp}`;
|
|
120
|
+
const watchlistId = params.watchlistId || '{watchlistId}';
|
|
121
|
+
const version = params.version || '{version}';
|
|
122
|
+
|
|
123
|
+
const legacyPathMap = {
|
|
124
|
+
// Signed-in user data (CID-based)
|
|
125
|
+
notifications: firebaseUid ? `user_notifications/${firebaseUid}/notifications` : `user_notifications/${cid}/notifications`,
|
|
126
|
+
notificationsCounters: firebaseUid ? `user_notifications/${firebaseUid}/counters` : `user_notifications/${cid}/counters`,
|
|
127
|
+
alerts: `user_alerts/${cid}/alerts`,
|
|
128
|
+
alertsCounters: `user_alerts/${cid}/counters`,
|
|
129
|
+
watchlists: `user_watchlists/${cid}/lists`,
|
|
130
|
+
subscriptions: `watchlist_subscriptions/${cid}/alerts`,
|
|
131
|
+
verification: config.verificationsCollection ? `${config.verificationsCollection}/${username}` : `user_verifications/${username}`,
|
|
132
|
+
syncRequests: `user_sync_requests/${cid}/requests`,
|
|
133
|
+
syncStatus: `user_sync_requests/${cid}/global/latest`,
|
|
134
|
+
portfolio: config.signedInUsersCollection ? `${config.signedInUsersCollection}/19M/snapshots/${date}/parts` : `signed_in_users/19M/snapshots/${date}/parts`,
|
|
135
|
+
tradeHistory: config.signedInHistoryCollection ? `${config.signedInHistoryCollection}/19M/snapshots/${date}/parts` : `signed_in_user_history/19M/snapshots/${date}/parts`,
|
|
136
|
+
socialPosts: `signed_in_users_social/${cid}/posts`,
|
|
137
|
+
|
|
138
|
+
// Popular Investor data (PI CID-based)
|
|
139
|
+
piFetchRequests: `pi_fetch_requests/${piCid}/requests/${requestId}`,
|
|
140
|
+
piFetchStatus: `pi_fetch_requests/${piCid}/global/latest`,
|
|
141
|
+
piUserFetchRequests: `pi_fetch_requests/${piCid}/user_requests/${cid}`,
|
|
142
|
+
piReviews: `pi_reviews/${reviewId}`,
|
|
143
|
+
piSocialPosts: `pi_social_posts/${piCid}/posts/${postId}`,
|
|
144
|
+
piProfileViews: `profile_views/${piCid}_${date}`,
|
|
145
|
+
piIndividualViews: `profile_views/individual_views/views/${viewId}`,
|
|
146
|
+
|
|
147
|
+
// Public/system data (no migration needed, but documented)
|
|
148
|
+
publicWatchlists: `public_watchlists/${watchlistId}/versions/${version}`,
|
|
149
|
+
userGcidMappings: `user_gcid_mappings/${cid}`,
|
|
150
|
+
|
|
151
|
+
// Root data collections (no migration - these are date-based and stay as-is)
|
|
152
|
+
// These are populated by task engines and should remain in their current format
|
|
153
|
+
signedInUserPortfolioRoot: `SignedInUserPortfolioData/${date}/${cid}`,
|
|
154
|
+
signedInUserTradeHistoryRoot: `SignedInUserTradeHistoryData/${date}/${cid}`,
|
|
155
|
+
signedInUserSocialRoot: `SignedInUserSocialPostData/${date}/${cid}`,
|
|
156
|
+
popularInvestorPortfolioRoot: `PopularInvestorPortfolioData/${date}/${piCid}`,
|
|
157
|
+
popularInvestorTradeHistoryRoot: `PopularInvestorTradeHistoryData/${date}/${piCid}`,
|
|
158
|
+
popularInvestorSocialRoot: `PopularInvestorSocialPostData/${date}/${piCid}`,
|
|
159
|
+
piPortfoliosDeep: `pi_portfolios_deep/19M/snapshots/${date}/parts`,
|
|
160
|
+
piPortfoliosOverall: `pi_portfolios_overall/19M/snapshots/${date}/parts`
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return legacyPathMap[dataType] || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Read with migration - tries new path first, falls back to legacy, auto-migrates if found
|
|
168
|
+
* @param {object} db - Firestore instance
|
|
169
|
+
* @param {string} category - Registry category
|
|
170
|
+
* @param {string} subcategory - Subcategory name
|
|
171
|
+
* @param {object} params - Path parameters
|
|
172
|
+
* @param {object} options - Read options
|
|
173
|
+
* @param {boolean} options.isCollection - If true, reads as collection; if false, reads as document
|
|
174
|
+
* @param {string} options.dataType - Data type for legacy path lookup
|
|
175
|
+
* @param {object} options.config - Configuration object
|
|
176
|
+
* @param {object} options.logger - Logger instance
|
|
177
|
+
* @param {string} options.documentId - Document ID (for collections)
|
|
178
|
+
* @returns {Promise<object|null>} - Document data or snapshot, with migration info
|
|
179
|
+
*/
|
|
180
|
+
async function readWithMigration(db, category, subcategory, params, options = {}) {
|
|
181
|
+
const {
|
|
182
|
+
isCollection = false,
|
|
183
|
+
dataType = null,
|
|
184
|
+
config = {},
|
|
185
|
+
logger = null,
|
|
186
|
+
documentId = null
|
|
187
|
+
} = options;
|
|
188
|
+
|
|
189
|
+
const userCid = params.cid || params.userCid;
|
|
190
|
+
|
|
191
|
+
// Get new path from registry
|
|
192
|
+
let newPath;
|
|
193
|
+
try {
|
|
194
|
+
newPath = getNewPath(category, subcategory, params);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (logger) logger.log('WARN', `[readWithMigration] Could not resolve new path for ${category}/${subcategory}: ${error.message}`);
|
|
197
|
+
newPath = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Try new path first
|
|
201
|
+
if (newPath) {
|
|
202
|
+
try {
|
|
203
|
+
if (isCollection) {
|
|
204
|
+
const collectionRef = db.collection(newPath);
|
|
205
|
+
const snapshot = await collectionRef.get();
|
|
206
|
+
if (!snapshot.empty) {
|
|
207
|
+
if (logger) logger.log('INFO', `[readWithMigration] Found data in new path: ${newPath}`);
|
|
208
|
+
return { snapshot, source: 'new', path: newPath };
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
const docRef = documentId
|
|
212
|
+
? db.collection(newPath).doc(documentId)
|
|
213
|
+
: db.doc(newPath);
|
|
214
|
+
const doc = await docRef.get();
|
|
215
|
+
if (doc.exists) {
|
|
216
|
+
if (logger) logger.log('INFO', `[readWithMigration] Found data in new path: ${docRef.path}`);
|
|
217
|
+
return { data: doc.data(), source: 'new', path: docRef.path };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (newError) {
|
|
221
|
+
if (logger) logger.log('WARN', `[readWithMigration] Error reading from new path ${newPath}: ${newError.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Fallback to legacy path
|
|
226
|
+
if (dataType && userCid) {
|
|
227
|
+
const legacyPathTemplate = getLegacyPath(dataType, userCid, config, params);
|
|
228
|
+
if (legacyPathTemplate) {
|
|
229
|
+
try {
|
|
230
|
+
// Resolve legacy path (may have additional params like date, username)
|
|
231
|
+
// Replace placeholders in legacy path template
|
|
232
|
+
let legacyPath = legacyPathTemplate;
|
|
233
|
+
for (const [key, value] of Object.entries(params)) {
|
|
234
|
+
legacyPath = legacyPath.replace(`{${key}}`, String(value));
|
|
235
|
+
}
|
|
236
|
+
// Also replace common placeholders
|
|
237
|
+
legacyPath = legacyPath.replace(/{date}/g, params.date || '{date}');
|
|
238
|
+
legacyPath = legacyPath.replace(/{requestId}/g, params.requestId || documentId || '{requestId}');
|
|
239
|
+
legacyPath = legacyPath.replace(/{username}/g, params.username || '{username}');
|
|
240
|
+
legacyPath = legacyPath.replace(/{piCid}/g, params.piCid || userCid);
|
|
241
|
+
legacyPath = legacyPath.replace(/{userCid}/g, String(userCid));
|
|
242
|
+
legacyPath = legacyPath.replace(/{cid}/g, String(userCid));
|
|
243
|
+
legacyPath = legacyPath.replace(/{viewerCid}/g, params.viewerCid || '{viewerCid}');
|
|
244
|
+
legacyPath = legacyPath.replace(/{timestamp}/g, params.timestamp || Date.now().toString());
|
|
245
|
+
legacyPath = legacyPath.replace(/{viewId}/g, params.viewId || documentId || '{viewId}');
|
|
246
|
+
legacyPath = legacyPath.replace(/{reviewId}/g, params.reviewId || documentId || `{piCid}_{userCid}`);
|
|
247
|
+
legacyPath = legacyPath.replace(/{postId}/g, params.postId || documentId || '{postId}');
|
|
248
|
+
legacyPath = legacyPath.replace(/{watchlistId}/g, params.watchlistId || '{watchlistId}');
|
|
249
|
+
legacyPath = legacyPath.replace(/{version}/g, params.version || '{version}');
|
|
250
|
+
|
|
251
|
+
if (isCollection) {
|
|
252
|
+
const legacyCollectionRef = db.collection(legacyPath);
|
|
253
|
+
const legacySnapshot = await legacyCollectionRef.get();
|
|
254
|
+
if (!legacySnapshot.empty) {
|
|
255
|
+
if (logger) logger.log('INFO', `[readWithMigration] Found data in legacy path: ${legacyPath}, will migrate`);
|
|
256
|
+
|
|
257
|
+
// Auto-migrate if new path is available
|
|
258
|
+
if (newPath) {
|
|
259
|
+
await migrateCollectionData(db, legacySnapshot, newPath, dataType, logger);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { snapshot: legacySnapshot, source: 'legacy', path: legacyPath, migrated: !!newPath };
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
const legacyDocRef = documentId
|
|
266
|
+
? db.collection(legacyPath).doc(documentId)
|
|
267
|
+
: db.doc(legacyPath);
|
|
268
|
+
const legacyDoc = await legacyDocRef.get();
|
|
269
|
+
if (legacyDoc.exists) {
|
|
270
|
+
if (logger) logger.log('INFO', `[readWithMigration] Found data in legacy path: ${legacyDocRef.path}, will migrate`);
|
|
271
|
+
|
|
272
|
+
// Auto-migrate if new path is available
|
|
273
|
+
if (newPath) {
|
|
274
|
+
await migrateDocumentData(db, legacyDoc, newPath, documentId, logger);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { data: legacyDoc.data(), source: 'legacy', path: legacyDocRef.path, migrated: !!newPath };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (legacyError) {
|
|
281
|
+
if (logger) logger.log('WARN', `[readWithMigration] Error reading from legacy path: ${legacyError.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Migrate document data from legacy to new path
|
|
291
|
+
* @param {object} db - Firestore instance
|
|
292
|
+
* @param {object} legacyDoc - Legacy document snapshot
|
|
293
|
+
* @param {string} newPath - New path (collection path, not full document path)
|
|
294
|
+
* @param {string} documentId - Document ID
|
|
295
|
+
* @param {object} logger - Logger instance
|
|
296
|
+
* @returns {Promise<void>}
|
|
297
|
+
*/
|
|
298
|
+
async function migrateDocumentData(db, legacyDoc, newPath, documentId, logger) {
|
|
299
|
+
try {
|
|
300
|
+
const data = legacyDoc.data();
|
|
301
|
+
const newDocRef = documentId
|
|
302
|
+
? db.collection(newPath).doc(documentId)
|
|
303
|
+
: db.doc(newPath);
|
|
304
|
+
|
|
305
|
+
// Check if already migrated
|
|
306
|
+
const existingDoc = await newDocRef.get();
|
|
307
|
+
if (existingDoc.exists) {
|
|
308
|
+
if (logger) logger.log('INFO', `[migrateDocumentData] Data already exists in new path: ${newDocRef.path}`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Migrate data
|
|
313
|
+
await newDocRef.set({
|
|
314
|
+
...data,
|
|
315
|
+
_migratedAt: FieldValue.serverTimestamp(),
|
|
316
|
+
_migratedFrom: legacyDoc.ref.path
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (logger) logger.log('SUCCESS', `[migrateDocumentData] Migrated document from ${legacyDoc.ref.path} to ${newDocRef.path}`);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (logger) logger.log('ERROR', `[migrateDocumentData] Migration failed: ${error.message}`);
|
|
322
|
+
// Don't throw - migration failures shouldn't break reads
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Migrate collection data from legacy to new path
|
|
328
|
+
* @param {object} db - Firestore instance
|
|
329
|
+
* @param {object} legacySnapshot - Legacy collection snapshot
|
|
330
|
+
* @param {string} newPath - New collection path
|
|
331
|
+
* @param {string} dataType - Data type
|
|
332
|
+
* @param {object} logger - Logger instance
|
|
333
|
+
* @returns {Promise<void>}
|
|
334
|
+
*/
|
|
335
|
+
async function migrateCollectionData(db, legacySnapshot, newPath, dataType, logger) {
|
|
336
|
+
try {
|
|
337
|
+
const batch = db.batch();
|
|
338
|
+
let migratedCount = 0;
|
|
339
|
+
|
|
340
|
+
for (const doc of legacySnapshot.docs) {
|
|
341
|
+
const newDocRef = db.collection(newPath).doc(doc.id);
|
|
342
|
+
|
|
343
|
+
// Check if already migrated
|
|
344
|
+
const existingDoc = await newDocRef.get();
|
|
345
|
+
if (existingDoc.exists) {
|
|
346
|
+
continue; // Skip if already migrated
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Add to batch
|
|
350
|
+
batch.set(newDocRef, {
|
|
351
|
+
...doc.data(),
|
|
352
|
+
_migratedAt: FieldValue.serverTimestamp(),
|
|
353
|
+
_migratedFrom: doc.ref.path
|
|
354
|
+
});
|
|
355
|
+
migratedCount++;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (migratedCount > 0) {
|
|
359
|
+
await batch.commit();
|
|
360
|
+
if (logger) logger.log('SUCCESS', `[migrateCollectionData] Migrated ${migratedCount} documents from legacy to ${newPath}`);
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (logger) logger.log('ERROR', `[migrateCollectionData] Migration failed: ${error.message}`);
|
|
364
|
+
// Don't throw - migration failures shouldn't break reads
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Write with dual write support (writes to both new and legacy during migration)
|
|
370
|
+
* @param {object} db - Firestore instance
|
|
371
|
+
* @param {string} category - Registry category
|
|
372
|
+
* @param {string} subcategory - Subcategory name
|
|
373
|
+
* @param {object} params - Path parameters
|
|
374
|
+
* @param {object} data - Data to write
|
|
375
|
+
* @param {object} options - Write options
|
|
376
|
+
* @param {boolean} options.isCollection - If true, writes as collection; if false, writes as document
|
|
377
|
+
* @param {boolean} options.merge - If true, merges data instead of overwriting
|
|
378
|
+
* @param {string} options.dataType - Data type for legacy path lookup
|
|
379
|
+
* @param {object} options.config - Configuration object
|
|
380
|
+
* @param {string} options.documentId - Document ID (required for collections)
|
|
381
|
+
* @param {boolean} options.dualWrite - If true, writes to both paths (default: true during migration)
|
|
382
|
+
* @returns {Promise<void>}
|
|
383
|
+
*/
|
|
384
|
+
async function writeWithMigration(db, category, subcategory, params, data, options = {}) {
|
|
385
|
+
const {
|
|
386
|
+
isCollection = false,
|
|
387
|
+
merge = false,
|
|
388
|
+
dataType = null,
|
|
389
|
+
config = {},
|
|
390
|
+
documentId = null,
|
|
391
|
+
dualWrite = true // Default to dual write during migration period
|
|
392
|
+
} = options;
|
|
393
|
+
|
|
394
|
+
const userCid = params.cid || params.userCid;
|
|
395
|
+
|
|
396
|
+
// Get new path
|
|
397
|
+
const newPath = getNewPath(category, subcategory, params);
|
|
398
|
+
|
|
399
|
+
// Get legacy path if dual write is enabled
|
|
400
|
+
let legacyPath = null;
|
|
401
|
+
if (dualWrite && dataType && userCid) {
|
|
402
|
+
legacyPath = getLegacyPath(dataType, userCid, config);
|
|
403
|
+
if (legacyPath) {
|
|
404
|
+
legacyPath = resolvePath(legacyPath, params);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const batch = db.batch();
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
// Write to new path
|
|
412
|
+
if (isCollection) {
|
|
413
|
+
if (!documentId) {
|
|
414
|
+
throw new Error('Collection writes require documentId');
|
|
415
|
+
}
|
|
416
|
+
const newRef = db.collection(newPath).doc(documentId);
|
|
417
|
+
if (merge) {
|
|
418
|
+
batch.set(newRef, data, { merge: true });
|
|
419
|
+
} else {
|
|
420
|
+
batch.set(newRef, data);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
const newRef = documentId
|
|
424
|
+
? db.collection(newPath).doc(documentId)
|
|
425
|
+
: db.doc(newPath);
|
|
426
|
+
if (merge) {
|
|
427
|
+
batch.set(newRef, data, { merge: true });
|
|
428
|
+
} else {
|
|
429
|
+
batch.set(newRef, data);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Write to legacy path if dual write is enabled
|
|
434
|
+
if (legacyPath) {
|
|
435
|
+
if (isCollection) {
|
|
436
|
+
if (!documentId) {
|
|
437
|
+
throw new Error('Collection writes require documentId');
|
|
438
|
+
}
|
|
439
|
+
const legacyRef = db.collection(legacyPath).doc(documentId);
|
|
440
|
+
if (merge) {
|
|
441
|
+
batch.set(legacyRef, data, { merge: true });
|
|
442
|
+
} else {
|
|
443
|
+
batch.set(legacyRef, data);
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
const legacyRef = documentId
|
|
447
|
+
? db.collection(legacyPath).doc(documentId)
|
|
448
|
+
: db.doc(legacyPath);
|
|
449
|
+
if (merge) {
|
|
450
|
+
batch.set(legacyRef, data, { merge: true });
|
|
451
|
+
} else {
|
|
452
|
+
batch.set(legacyRef, data);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await batch.commit();
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error('[writeWithMigration] Error writing data:', error);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get collection path helper (for backward compatibility)
|
|
466
|
+
* @param {object} collectionRegistry - Collection registry (not used, kept for compatibility)
|
|
467
|
+
* @param {string} category - Registry category
|
|
468
|
+
* @param {string} subcategory - Subcategory name
|
|
469
|
+
* @param {object} params - Path parameters
|
|
470
|
+
* @returns {string} - Resolved path
|
|
471
|
+
*/
|
|
472
|
+
function getCollectionPathHelper(collectionRegistry, category, subcategory, params = {}) {
|
|
473
|
+
return getNewPath(category, subcategory, params);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
module.exports = {
|
|
477
|
+
getCidFromFirebaseUid,
|
|
478
|
+
getNewPath,
|
|
479
|
+
getLegacyPath,
|
|
480
|
+
readWithMigration,
|
|
481
|
+
writeWithMigration,
|
|
482
|
+
migrateDocumentData,
|
|
483
|
+
migrateCollectionData,
|
|
484
|
+
getCollectionPath: getCollectionPathHelper // For backward compatibility
|
|
485
|
+
};
|
|
486
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview User Status Helpers
|
|
3
|
+
* Functions to check user status (PI, signed-in, etc.)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { findLatestRankingsDate } = require('./data_lookup_helpers');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a signed-in user is also a Popular Investor
|
|
10
|
+
* Returns ranking entry if found, null otherwise
|
|
11
|
+
* Checks dev overrides first for pretendToBePI flag
|
|
12
|
+
* @param {Firestore} db - Firestore instance
|
|
13
|
+
* @param {string|number} userCid - User CID
|
|
14
|
+
* @param {object} config - Configuration object
|
|
15
|
+
* @param {object} logger - Logger instance (optional)
|
|
16
|
+
* @returns {Promise<object|null>} - Ranking entry or null
|
|
17
|
+
*/
|
|
18
|
+
async function checkIfUserIsPI(db, userCid, config, logger = null) {
|
|
19
|
+
try {
|
|
20
|
+
// Check dev override first (for developer accounts)
|
|
21
|
+
const { getDevOverride } = require('../dev_helpers');
|
|
22
|
+
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
23
|
+
|
|
24
|
+
if (devOverride && devOverride.enabled && devOverride.pretendToBePI) {
|
|
25
|
+
// Generate fake ranking entry for dev testing
|
|
26
|
+
const fakeRankEntry = {
|
|
27
|
+
CustomerId: Number(userCid),
|
|
28
|
+
UserName: 'Dev Test PI',
|
|
29
|
+
AUMValue: 500000 + Math.floor(Math.random() * 1000000), // Random AUM between 500k-1.5M
|
|
30
|
+
Copiers: 150 + Math.floor(Math.random() * 200), // Random copiers between 150-350
|
|
31
|
+
RiskScore: 3 + Math.floor(Math.random() * 3), // Random risk score 3-5
|
|
32
|
+
Gain: 25 + Math.floor(Math.random() * 50), // Random gain 25-75%
|
|
33
|
+
WinRatio: 50 + Math.floor(Math.random() * 20), // Random win ratio 50-70%
|
|
34
|
+
Trades: 500 + Math.floor(Math.random() * 1000) // Random trades 500-1500
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (logger && logger.log) {
|
|
38
|
+
logger.log('INFO', `[checkIfUserIsPI] DEV OVERRIDE: User ${userCid} pretending to be PI with fake ranking data`);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(`[checkIfUserIsPI] DEV OVERRIDE: User ${userCid} pretending to be PI with fake ranking data`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return fakeRankEntry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Otherwise, check real rankings
|
|
47
|
+
const rankingsCollection = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
|
|
48
|
+
const rankingsDate = await findLatestRankingsDate(db, rankingsCollection, 30);
|
|
49
|
+
|
|
50
|
+
if (!rankingsDate) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rankingsRef = db.collection(rankingsCollection).doc(rankingsDate);
|
|
55
|
+
const rankingsDoc = await rankingsRef.get();
|
|
56
|
+
|
|
57
|
+
if (!rankingsDoc.exists) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rankingsData = rankingsDoc.data();
|
|
62
|
+
const rankingsItems = rankingsData.Items || [];
|
|
63
|
+
|
|
64
|
+
// Find user in rankings
|
|
65
|
+
const userRankEntry = rankingsItems.find(item => String(item.CustomerId) === String(userCid));
|
|
66
|
+
|
|
67
|
+
return userRankEntry || null;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('[checkIfUserIsPI] Error checking if user is PI:', error);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
checkIfUserIsPI
|
|
76
|
+
};
|
|
77
|
+
|