bulltrackers-module 1.0.537 → 1.0.539

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.
@@ -1,46 +1,165 @@
1
1
  /**
2
2
  * @fileoverview Social Posts Data Helpers
3
3
  * Handles user social posts endpoints
4
+ * UPDATED: Uses collection registry to read from root data collection
4
5
  */
5
6
 
7
+ const { findLatestRankingsDate } = require('../core/data_lookup_helpers');
8
+ const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
9
+
10
+ /**
11
+ * Find the latest available date for signed-in user social posts
12
+ * Searches backwards from today up to maxDaysBack days
13
+ * @param {Firestore} db - Firestore instance
14
+ * @param {string|number} userCid - User CID
15
+ * @param {object} collectionRegistry - Collection registry (injected at runtime)
16
+ * @param {number} maxDaysBack - Maximum days to search backwards (default: 30)
17
+ * @returns {Promise<string|null>} - Date string (YYYY-MM-DD) or null if not found
18
+ */
19
+ async function findLatestSocialDate(db, userCid, collectionRegistry, maxDaysBack = 30) {
20
+ const today = new Date();
21
+
22
+ // Get collection name from registry
23
+ let collectionName = 'SignedInUserSocialPostData'; // Default fallback
24
+ if (collectionRegistry && collectionRegistry.getCollectionPath) {
25
+ try {
26
+ // Extract collection name from registry path template
27
+ const samplePath = collectionRegistry.getCollectionPath('rootData', 'signedInUserSocial', {
28
+ date: '2025-01-01',
29
+ cid: '123'
30
+ });
31
+ collectionName = samplePath.split('/')[0]; // Extract collection name
32
+ } catch (e) {
33
+ // Use default collection name
34
+ }
35
+ }
36
+
37
+ for (let i = 0; i < maxDaysBack; i++) {
38
+ const checkDate = new Date(today);
39
+ checkDate.setDate(checkDate.getDate() - i);
40
+ const dateStr = checkDate.toISOString().split('T')[0];
41
+
42
+ try {
43
+ // Construct path: SignedInUserSocialPostData/{date}/{cid}/{cid}
44
+ const socialDocRef = db.collection(collectionName)
45
+ .doc(dateStr)
46
+ .collection(String(userCid))
47
+ .doc(String(userCid));
48
+
49
+ const socialDoc = await socialDocRef.get();
50
+
51
+ if (socialDoc.exists) {
52
+ const data = socialDoc.data();
53
+ if (data.posts && Object.keys(data.posts).length > 0) {
54
+ return dateStr; // Found social posts for this date
55
+ }
56
+ }
57
+ } catch (error) {
58
+ // Continue to next date if error
59
+ continue;
60
+ }
61
+ }
62
+
63
+ return null; // No social posts found in the last maxDaysBack days
64
+ }
65
+
6
66
  /**
7
67
  * GET /user/me/social-posts
8
- * Fetches the signed-in user's social posts
68
+ * Fetches the signed-in user's social posts from root data collection
9
69
  */
10
70
  async function getUserSocialPosts(req, res, dependencies, config) {
11
- const { db, logger } = dependencies;
12
- const { userCid } = req.query;
71
+ const { db, logger, collectionRegistry } = dependencies;
72
+ const { userCid, date } = req.query;
13
73
 
14
74
  if (!userCid) {
15
75
  return res.status(400).json({ error: "Missing userCid" });
16
76
  }
17
77
 
18
78
  try {
19
- const { signedInSocialCollection } = config;
20
- const socialCollection = signedInSocialCollection || 'signed_in_users_social';
21
-
22
- // Fetch posts from signed_in_users_social/{cid}/posts
23
- const postsRef = db.collection(socialCollection)
24
- .doc(String(userCid))
25
- .collection('posts');
26
-
27
- const postsSnapshot = await postsRef
28
- .orderBy('createdAt', 'desc')
29
- .limit(50) // Limit to 50 most recent posts
30
- .get();
31
-
32
- const posts = [];
33
- postsSnapshot.forEach(doc => {
34
- posts.push({
35
- id: doc.id,
36
- ...doc.data()
79
+ // Check for dev override impersonation
80
+ const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
81
+ const devOverride = await getDevOverride(db, userCid, config, logger);
82
+ const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
83
+
84
+ const today = new Date().toISOString().split('T')[0];
85
+
86
+ // Determine which date to use
87
+ let dataDate = date || today;
88
+ let isFallback = false;
89
+
90
+ // If no specific date requested, find latest available date
91
+ if (!date) {
92
+ dataDate = await findLatestSocialDate(db, effectiveCid, collectionRegistry, 30);
93
+ if (!dataDate) {
94
+ return res.status(404).json({
95
+ error: "Social posts not found for this user (checked last 30 days)",
96
+ effectiveCid: effectiveCid,
97
+ isImpersonating: isImpersonating || false
98
+ });
99
+ }
100
+ isFallback = dataDate !== today;
101
+ if (isFallback) {
102
+ logger.log('INFO', `[getUserSocialPosts] Using fallback date ${dataDate} for effective CID ${effectiveCid} (today: ${today})`);
103
+ }
104
+ }
105
+
106
+ // Get collection name from registry
107
+ let collectionName = 'SignedInUserSocialPostData'; // Default fallback
108
+ if (collectionRegistry && collectionRegistry.getCollectionPath) {
109
+ try {
110
+ // Extract collection name from registry path template
111
+ const samplePath = collectionRegistry.getCollectionPath('rootData', 'signedInUserSocial', {
112
+ date: '2025-01-01',
113
+ cid: '123'
114
+ });
115
+ collectionName = samplePath.split('/')[0]; // Extract collection name
116
+ } catch (e) {
117
+ logger.log('WARN', `[getUserSocialPosts] Failed to get collection name from registry, using default: ${e.message}`);
118
+ }
119
+ }
120
+
121
+ // Construct path: SignedInUserSocialPostData/{date}/{cid}/{cid}
122
+ const socialDocRef = db.collection(collectionName)
123
+ .doc(dataDate)
124
+ .collection(String(effectiveCid))
125
+ .doc(String(effectiveCid));
126
+
127
+ const socialDoc = await socialDocRef.get();
128
+
129
+ if (!socialDoc.exists) {
130
+ return res.status(404).json({
131
+ error: "Social posts not found",
132
+ date: dataDate,
133
+ effectiveCid: effectiveCid,
134
+ isImpersonating: isImpersonating || false
37
135
  });
38
- });
136
+ }
137
+
138
+ const socialData = socialDoc.data();
139
+ const postsMap = socialData.posts || {};
140
+
141
+ // Convert posts map to array and sort by createdAt (descending)
142
+ const posts = Object.entries(postsMap)
143
+ .map(([postId, postData]) => ({
144
+ id: postId,
145
+ ...postData
146
+ }))
147
+ .sort((a, b) => {
148
+ const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
149
+ const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
150
+ return dateB - dateA; // Descending order
151
+ })
152
+ .slice(0, 50); // Limit to 50 most recent posts
39
153
 
40
154
  return res.status(200).json({
41
155
  posts,
42
156
  count: posts.length,
43
- userCid: String(userCid)
157
+ date: dataDate,
158
+ isFallback: isFallback,
159
+ requestedDate: date || today,
160
+ userCid: String(userCid),
161
+ effectiveCid: effectiveCid,
162
+ isImpersonating: isImpersonating || false
44
163
  });
45
164
 
46
165
  } catch (error) {
@@ -5,17 +5,62 @@
5
5
 
6
6
  const { FieldValue } = require('@google-cloud/firestore');
7
7
 
8
+ /**
9
+ * Get the detail field name for a data type
10
+ * @param {string} dataType - Data type identifier
11
+ * @returns {string} - Detail field name in the index document
12
+ */
13
+ function getDetailFieldName(dataType) {
14
+ const dataTypeMap = {
15
+ 'signedInUserPortfolio': 'signedInUserPortfolio',
16
+ 'signedInUserHistory': 'signedInUserHistory',
17
+ 'signedInUserSocial': 'signedInSocial',
18
+ 'piPortfolios': 'piPortfolios',
19
+ 'piHistory': 'piHistory',
20
+ 'piSocial': 'piSocial',
21
+ 'normalPortfolios': 'normalPortfolio',
22
+ 'normalHistory': 'normalHistory',
23
+ 'speculatorPortfolios': 'speculatorPortfolio',
24
+ 'speculatorHistory': 'speculatorHistory'
25
+ };
26
+ return dataTypeMap[dataType] || dataType;
27
+ }
28
+
8
29
  /**
9
30
  * Check if root data is already indexed for a specific date
10
31
  * @param {Firestore} db - Firestore instance
11
32
  * @param {string} dateStr - Date string (YYYY-MM-DD)
12
33
  * @param {string} availabilityCollection - Collection name for root data index
13
- * @returns {Promise<boolean>} - True if already indexed
34
+ * @param {Array<string>} dataTypesRun - Optional array of data types to verify are indexed (e.g., ['signedInUserPortfolio', 'piSocial'])
35
+ * @returns {Promise<boolean>} - True if already indexed (and all requested data types are indexed if provided)
14
36
  */
15
- async function isRootDataIndexed(db, dateStr, availabilityCollection = 'system_root_data_index') {
37
+ async function isRootDataIndexed(db, dateStr, availabilityCollection = 'system_root_data_index', dataTypesRun = []) {
16
38
  try {
17
39
  const indexDoc = await db.collection(availabilityCollection).doc(dateStr).get();
18
- return indexDoc.exists;
40
+ if (!indexDoc.exists) {
41
+ return false;
42
+ }
43
+
44
+ // If specific data types were requested, verify they are actually indexed
45
+ if (dataTypesRun && dataTypesRun.length > 0) {
46
+ const indexData = indexDoc.data();
47
+ const details = indexData?.details || {};
48
+
49
+ // Check if all requested data types are indexed (have true values)
50
+ // If any are false or missing, return false (needs indexing)
51
+ for (const dataType of dataTypesRun) {
52
+ const detailField = getDetailFieldName(dataType);
53
+ if (details[detailField] !== true) {
54
+ // At least one requested data type is not indexed
55
+ return false;
56
+ }
57
+ }
58
+ // All requested data types are indexed as true
59
+ return true;
60
+ }
61
+
62
+ // No specific data types requested - just check if document exists
63
+ return true;
19
64
  } catch (error) {
20
65
  // If we can't check, assume not indexed to be safe
21
66
  return false;
@@ -39,11 +84,43 @@ async function conditionallyRunRootDataIndexer({ db, logger, dateStr, rootDataIn
39
84
  const { runRootDataIndexer } = require('../../root-data-indexer/index');
40
85
  const availabilityCollection = rootDataIndexerConfig?.availabilityCollection || 'system_root_data_index';
41
86
 
42
- // Check if already indexed
43
- const alreadyIndexed = await isRootDataIndexed(db, dateStr, availabilityCollection);
44
- if (alreadyIndexed) {
45
- logger.log('INFO', `[RootDataIndexer] Root data already indexed for ${dateStr}, skipping`);
46
- return false;
87
+ // IMPORTANT: Only index data types that were successfully stored (dataTypesRun contains only successful operations)
88
+ // If a write failed, it won't be in dataTypesRun, so we won't try to index it
89
+ // However, we still run the indexer to check/update the types that did succeed
90
+
91
+ // Check if already indexed (verify specific data types if provided)
92
+ // Only check the data types that were actually run - if they're all already true, skip indexing (never overwrite true values)
93
+ if (dataTypesRun.length > 0) {
94
+ const alreadyIndexed = await isRootDataIndexed(db, dateStr, availabilityCollection, dataTypesRun);
95
+ if (alreadyIndexed) {
96
+ logger.log('INFO', `[RootDataIndexer] All requested data types (${dataTypesRun.join(', ')}) are already indexed as true for ${dateStr}, skipping (never overwrite true values)`);
97
+ return false;
98
+ }
99
+
100
+ // If document exists but data types aren't indexed, log it
101
+ try {
102
+ const indexDoc = await db.collection(availabilityCollection).doc(dateStr).get();
103
+ if (indexDoc.exists) {
104
+ const indexData = indexDoc.data();
105
+ const details = indexData?.details || {};
106
+ const missingTypes = dataTypesRun.filter(dt => {
107
+ const detailField = getDetailFieldName(dt);
108
+ return details[detailField] !== true;
109
+ });
110
+ if (missingTypes.length > 0) {
111
+ logger.log('INFO', `[RootDataIndexer] Index document exists for ${dateStr} but data types (${missingTypes.join(', ')}) are not indexed. Re-indexing...`);
112
+ }
113
+ }
114
+ } catch (e) {
115
+ // Ignore errors checking document
116
+ }
117
+ } else {
118
+ // No specific data types - check if document exists (legacy behavior)
119
+ const alreadyIndexed = await isRootDataIndexed(db, dateStr, availabilityCollection, []);
120
+ if (alreadyIndexed) {
121
+ logger.log('INFO', `[RootDataIndexer] Root data already indexed for ${dateStr}, skipping`);
122
+ return false;
123
+ }
47
124
  }
48
125
 
49
126
  // Use Firestore transaction to ensure only one instance runs the indexer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.537",
3
+ "version": "1.0.539",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [