Skip to content

Commit 86760d4

Browse files
committed
Deeper check for queries, more consideration around __rerum and _id properties
1 parent 82a46d2 commit 86760d4

1 file changed

Lines changed: 169 additions & 8 deletions

File tree

cache/index.js

Lines changed: 169 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,30 +316,76 @@ class LRUCache {
316316

317317
/**
318318
* Check if an object contains all properties specified in a query
319+
* Supports MongoDB query operators like $or, $and, $in, $exists, $size, etc.
320+
* Note: __rerum is a protected property managed by RERUM and stripped from user requests,
321+
* so we handle it conservatively in invalidation logic.
319322
* @param {Object} obj - The object to check
320-
* @param {Object} queryProps - The properties to match
321-
* @returns {boolean} - True if object contains all query properties with matching values
323+
* @param {Object} queryProps - The properties to match (may include MongoDB operators)
324+
* @returns {boolean} - True if object matches the query conditions
322325
*/
323326
objectContainsProperties(obj, queryProps) {
324327
for (const [key, value] of Object.entries(queryProps)) {
325328
// Skip pagination and internal parameters
326-
if (key === 'limit' || key === 'skip' || key === '__rerum') {
329+
if (key === 'limit' || key === 'skip') {
327330
continue
328331
}
329332

330-
// Check if object has this property
331-
if (!(key in obj)) {
333+
// Skip __rerum and _id since they're server-managed properties
334+
// __rerum: RERUM metadata stripped from user requests
335+
// _id: MongoDB internal identifier not in request bodies
336+
// We can't reliably match on them during invalidation
337+
if (key === '__rerum' || key === '_id') {
338+
continue
339+
}
340+
341+
// Also skip nested __rerum and _id paths (e.g., "__rerum.history.next", "target._id")
342+
// These are server/database-managed metadata not present in request bodies
343+
if (key.startsWith('__rerum.') || key.includes('.__rerum.') || key.endsWith('.__rerum') ||
344+
key.startsWith('_id.') || key.includes('._id.') || key.endsWith('._id')) {
345+
continue
346+
}
347+
348+
// Handle MongoDB query operators
349+
if (key.startsWith('$')) {
350+
if (!this.evaluateOperator(obj, key, value)) {
351+
return false
352+
}
353+
continue
354+
}
355+
356+
// Handle nested operators on a field (e.g., {"body.title": {"$exists": true}})
357+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
358+
const hasOperators = Object.keys(value).some(k => k.startsWith('$'))
359+
if (hasOperators) {
360+
// Be conservative with operator queries on history fields (fallback safety)
361+
// Note: __rerum.* and _id.* are already skipped above
362+
if (key.includes('history')) {
363+
continue // Conservative - assume match for history-related queries
364+
}
365+
366+
// For non-metadata fields, try to evaluate the operators
367+
const fieldValue = this.getNestedProperty(obj, key)
368+
if (!this.evaluateFieldOperators(fieldValue, value)) {
369+
return false
370+
}
371+
continue
372+
}
373+
}
374+
375+
// Check if object has this property (handle both direct and nested paths)
376+
const objValue = this.getNestedProperty(obj, key)
377+
if (objValue === undefined && !(key in obj)) {
332378
return false
333379
}
334380

335381
// For simple values, check equality
336382
if (typeof value !== 'object' || value === null) {
337-
if (obj[key] !== value) {
383+
if (objValue !== value) {
338384
return false
339385
}
340386
} else {
341-
// For nested objects, recursively check
342-
if (!this.objectContainsProperties(obj[key], value)) {
387+
// For nested objects (no operators), recursively check
388+
if (typeof objValue !== 'object' || !this.objectContainsProperties(objValue, value)) {
343389
return false
344390
}
345391
}
@@ -348,6 +394,121 @@ class LRUCache {
348394
return true
349395
}
350396

397+
/**
398+
* Evaluate field-level operators like {"$exists": true, "$size": 0}
399+
* @param {*} fieldValue - The actual field value from the object
400+
* @param {Object} operators - Object containing operators and their values
401+
* @returns {boolean} - True if field satisfies all operators
402+
*/
403+
evaluateFieldOperators(fieldValue, operators) {
404+
for (const [op, opValue] of Object.entries(operators)) {
405+
switch (op) {
406+
case '$exists':
407+
const exists = fieldValue !== undefined
408+
if (exists !== opValue) return false
409+
break
410+
case '$size':
411+
if (!Array.isArray(fieldValue) || fieldValue.length !== opValue) {
412+
return false
413+
}
414+
break
415+
case '$ne':
416+
if (fieldValue === opValue) return false
417+
break
418+
case '$gt':
419+
if (!(fieldValue > opValue)) return false
420+
break
421+
case '$gte':
422+
if (!(fieldValue >= opValue)) return false
423+
break
424+
case '$lt':
425+
if (!(fieldValue < opValue)) return false
426+
break
427+
case '$lte':
428+
if (!(fieldValue <= opValue)) return false
429+
break
430+
default:
431+
// Unknown operator - be conservative
432+
return true
433+
}
434+
}
435+
return true
436+
}
437+
438+
/**
439+
* Get nested property value from an object using dot notation
440+
* @param {Object} obj - The object
441+
* @param {string} path - Property path (e.g., "target.@id" or "body.title.value")
442+
* @returns {*} Property value or undefined
443+
*/
444+
getNestedProperty(obj, path) {
445+
const keys = path.split('.')
446+
let current = obj
447+
448+
for (const key of keys) {
449+
if (current === null || current === undefined || typeof current !== 'object') {
450+
return undefined
451+
}
452+
current = current[key]
453+
}
454+
455+
return current
456+
}
457+
458+
/**
459+
* Evaluate MongoDB query operators
460+
* @param {Object} obj - The object or field value to evaluate against
461+
* @param {string} operator - The operator key (e.g., "$or", "$and", "$exists")
462+
* @param {*} value - The operator value
463+
* @returns {boolean} - True if the operator condition is satisfied
464+
*/
465+
evaluateOperator(obj, operator, value) {
466+
switch (operator) {
467+
case '$or':
468+
// $or: [condition1, condition2, ...]
469+
// Returns true if ANY condition matches
470+
if (!Array.isArray(value)) return false
471+
return value.some(condition => this.objectContainsProperties(obj, condition))
472+
473+
case '$and':
474+
// $and: [condition1, condition2, ...]
475+
// Returns true if ALL conditions match
476+
if (!Array.isArray(value)) return false
477+
return value.every(condition => this.objectContainsProperties(obj, condition))
478+
479+
case '$in':
480+
// Field value must be in the array
481+
// This is tricky - we need the actual field name context
482+
// For now, treat as potential match (conservative invalidation)
483+
return true
484+
485+
case '$exists':
486+
// {"field": {"$exists": true/false}}
487+
// We need field context - handled in parent function
488+
// This should not be called directly
489+
return true
490+
491+
case '$size':
492+
// {"field": {"$size": N}}
493+
// Array field must have exactly N elements
494+
// Conservative invalidation - return true
495+
return true
496+
497+
case '$ne':
498+
case '$gt':
499+
case '$gte':
500+
case '$lt':
501+
case '$lte':
502+
// Comparison operators - for invalidation, be conservative
503+
// If query uses these operators, invalidate (return true)
504+
return true
505+
506+
default:
507+
// Unknown operator - be conservative and invalidate
508+
return true
509+
}
510+
}
511+
351512
/**
352513
* Clear all cache entries
353514
*/

0 commit comments

Comments
 (0)