bulltrackers-module 1.0.591 → 1.0.592
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/alert-system/helpers/alert_helpers.js +6 -6
- package/functions/alert-system/index.js +1 -1
- package/functions/api-v2/helpers/data-fetchers/firestore.js +2218 -0
- package/functions/api-v2/helpers/task_engine_helper.js +51 -0
- package/functions/api-v2/index.js +36 -0
- package/functions/api-v2/middleware/identity_middleware.js +48 -0
- package/functions/api-v2/package.json +6 -0
- package/functions/api-v2/routes/alerts.js +168 -0
- package/functions/api-v2/routes/index.js +35 -0
- package/functions/api-v2/routes/notifications.js +38 -0
- package/functions/api-v2/routes/popular_investors.js +204 -0
- package/functions/api-v2/routes/profile.js +212 -0
- package/functions/api-v2/routes/reviews.js +72 -0
- package/functions/api-v2/routes/settings.js +71 -0
- package/functions/api-v2/routes/sync.js +132 -0
- package/functions/api-v2/routes/verification.js +47 -0
- package/functions/api-v2/routes/watchlists.js +148 -0
- package/functions/computation-system/helpers/computation_worker.js +1 -1
- package/functions/task-engine/helpers/popular_investor_helpers.js +2 -2
- package/index.js +6 -2
- package/package.json +2 -1
- package/functions/generic-api/admin-api/index.js +0 -895
- package/functions/generic-api/helpers/api_helpers.js +0 -457
- package/functions/generic-api/index.js +0 -204
- package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +0 -345
- package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +0 -320
- package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +0 -116
- package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +0 -171
- package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +0 -710
- package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +0 -109
- package/functions/generic-api/user-api/MIGRATION_PLAN.md +0 -499
- package/functions/generic-api/user-api/README_MIGRATION.md +0 -152
- package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +0 -106
- package/functions/generic-api/user-api/REFACTORING_STATUS.md +0 -85
- package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +0 -206
- package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +0 -126
- package/functions/generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
- package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
- package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
- package/functions/generic-api/user-api/helpers/collection_helpers.js +0 -193
- package/functions/generic-api/user-api/helpers/core/compression_helpers.js +0 -68
- package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
- package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
- package/functions/generic-api/user-api/helpers/data/computation_helpers.js +0 -503
- package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
- package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
- package/functions/generic-api/user-api/helpers/data/social_helpers.js +0 -174
- package/functions/generic-api/user-api/helpers/data_helpers.js +0 -87
- package/functions/generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
- package/functions/generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
- package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
- package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
- package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
- package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
- package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
- package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
- package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
- package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
- package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
- package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
- package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
- package/functions/generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
- package/functions/generic-api/user-api/index.js +0 -109
|
@@ -1,640 +0,0 @@
|
|
|
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
|
-
|