bulltrackers-module 1.0.786 → 1.0.787
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.
|
@@ -251,6 +251,43 @@ class StateRepository {
|
|
|
251
251
|
|
|
252
252
|
await batch.commit();
|
|
253
253
|
}
|
|
254
|
+
|
|
255
|
+
async findZombies(thresholdMinutes) {
|
|
256
|
+
const thresholdDate = new Date(Date.now() - (thresholdMinutes * 60000));
|
|
257
|
+
try {
|
|
258
|
+
// NOTE: Requires Composite Index on (status, lastUpdated)
|
|
259
|
+
const snapshot = await this.firestore.collection(this.collections.checkpoints)
|
|
260
|
+
.where('status', '==', 'running')
|
|
261
|
+
.where('lastUpdated', '<', thresholdDate)
|
|
262
|
+
.limit(50)
|
|
263
|
+
.get();
|
|
264
|
+
|
|
265
|
+
const zombies = [];
|
|
266
|
+
snapshot.forEach(doc => {
|
|
267
|
+
const data = doc.data();
|
|
268
|
+
zombies.push({
|
|
269
|
+
checkpointId: doc.id,
|
|
270
|
+
name: data.computationName,
|
|
271
|
+
date: data.date,
|
|
272
|
+
attempts: data.attempts || 0
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
return zombies;
|
|
276
|
+
} catch (e) {
|
|
277
|
+
this.logger.error(`[StateRepo] findZombies failed: ${e.message}`);
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async claimZombie(checkpointId) {
|
|
283
|
+
const docRef = this.firestore.collection(this.collections.checkpoints).doc(checkpointId);
|
|
284
|
+
await docRef.set({
|
|
285
|
+
status: 'recovering',
|
|
286
|
+
lastUpdated: new Date(),
|
|
287
|
+
// Uses atomic increment to track retry count
|
|
288
|
+
attempts: FieldValue.increment(1)
|
|
289
|
+
}, { merge: true });
|
|
290
|
+
}
|
|
254
291
|
}
|
|
255
292
|
|
|
256
293
|
module.exports = { StateRepository };
|
|
@@ -148,7 +148,7 @@ class QueryBuilder {
|
|
|
148
148
|
this._validateIdentifier(entityField);
|
|
149
149
|
conditions.push(`${entityField} IN UNNEST(@entities)`);
|
|
150
150
|
|
|
151
|
-
//
|
|
151
|
+
// [FIX] Ensure _isNumericType exists before calling
|
|
152
152
|
const entityColumn = schema.columns.find(c => c.name === entityField);
|
|
153
153
|
if (entityColumn && this._isNumericType(entityColumn.type)) {
|
|
154
154
|
params.entities = entities.map(e => parseInt(e, 10));
|
|
@@ -157,12 +157,10 @@ class QueryBuilder {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// 4. Additional WHERE conditions
|
|
160
|
+
// 4. Additional WHERE conditions
|
|
161
161
|
let whereIndex = 0;
|
|
162
162
|
for (const [column, value] of Object.entries(where)) {
|
|
163
|
-
// 🔒 SECURITY FIX: Validate identifier before injection
|
|
164
163
|
this._validateIdentifier(column);
|
|
165
|
-
|
|
166
164
|
const paramName = `where_${whereIndex++}`;
|
|
167
165
|
|
|
168
166
|
if (Array.isArray(value)) {
|
|
@@ -188,7 +186,6 @@ class QueryBuilder {
|
|
|
188
186
|
let orderClause = '';
|
|
189
187
|
if (orderBy) {
|
|
190
188
|
this._validateIdentifier(orderBy);
|
|
191
|
-
// Validate order direction strictly
|
|
192
189
|
const safeDir = ['ASC', 'DESC'].includes(orderDir.toUpperCase()) ? orderDir.toUpperCase() : 'DESC';
|
|
193
190
|
orderClause = `ORDER BY ${orderBy} ${safeDir}`;
|
|
194
191
|
}
|
|
@@ -207,15 +204,18 @@ class QueryBuilder {
|
|
|
207
204
|
return { sql, params, table, spec };
|
|
208
205
|
}
|
|
209
206
|
|
|
210
|
-
/**
|
|
211
|
-
* Security helper to prevent SQL injection in identifiers.
|
|
212
|
-
* Allows alphanumerics, underscores, and dots (for structs).
|
|
213
|
-
*/
|
|
214
207
|
_validateIdentifier(id) {
|
|
215
208
|
if (typeof id !== 'string' || !/^[a-zA-Z0-9_.]+$/.test(id)) {
|
|
216
209
|
throw new Error(`[QueryBuilder] Security Violation: Invalid identifier format '${id}'`);
|
|
217
210
|
}
|
|
218
211
|
}
|
|
212
|
+
|
|
213
|
+
// [FIX] Added missing helper method
|
|
214
|
+
_isNumericType(type) {
|
|
215
|
+
if (!type) return false;
|
|
216
|
+
const t = type.toUpperCase();
|
|
217
|
+
return ['INT64', 'INTEGER', 'FLOAT64', 'FLOAT', 'NUMERIC', 'BIGNUMERIC'].includes(t);
|
|
218
|
+
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
module.exports = { QueryBuilder };
|
|
221
|
+
module.exports = { QueryBuilder };
|