@@ -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