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
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|