@@ -706,3 +706,286 @@ describe('Cache Statistics', () => {
706706 expect ( stats . length ) . toBe ( initialSize + 1 )
707707 } )
708708} )
709+
710+ describe ( 'Cache Invalidation Tests' , ( ) => {
711+ beforeEach ( async ( ) => {
712+ await cache . clear ( )
713+ } )
714+
715+ afterEach ( async ( ) => {
716+ await cache . clear ( )
717+ } )
718+
719+ describe ( 'invalidateByObject' , ( ) => {
720+ it ( 'should invalidate matching query caches when object is created' , async ( ) => {
721+ // Cache a query for type=TestObject
722+ const queryKey = cache . generateKey ( 'query' , { body : { type : 'TestObject' } } )
723+ await cache . set ( queryKey , [ { id : '1' , type : 'TestObject' } ] )
724+
725+ // Verify cache exists
726+ let cached = await cache . get ( queryKey )
727+ expect ( cached ) . toBeTruthy ( )
728+
729+ // Create new object that matches the query
730+ const newObj = { id : '2' , type : 'TestObject' , name : 'Test' }
731+ const invalidatedKeys = new Set ( )
732+ const count = await cache . invalidateByObject ( newObj , invalidatedKeys )
733+
734+ // Verify cache was invalidated
735+ expect ( count ) . toBe ( 1 )
736+ expect ( invalidatedKeys . has ( queryKey ) ) . toBe ( true )
737+ cached = await cache . get ( queryKey )
738+ expect ( cached ) . toBeNull ( )
739+ } )
740+
741+ it ( 'should not invalidate non-matching query caches' , async ( ) => {
742+ // Cache a query for type=OtherObject
743+ const queryKey = cache . generateKey ( 'query' , { body : { type : 'OtherObject' } } )
744+ await cache . set ( queryKey , [ { id : '1' , type : 'OtherObject' } ] )
745+
746+ // Create object that doesn't match
747+ const newObj = { id : '2' , type : 'TestObject' }
748+ const count = await cache . invalidateByObject ( newObj )
749+
750+ // Verify cache was NOT invalidated
751+ expect ( count ) . toBe ( 0 )
752+ const cached = await cache . get ( queryKey )
753+ expect ( cached ) . toBeTruthy ( )
754+ } )
755+
756+ it ( 'should invalidate search caches' , async ( ) => {
757+ const searchKey = cache . generateKey ( 'search' , { body : { type : 'TestObject' } } )
758+ await cache . set ( searchKey , [ { id : '1' , type : 'TestObject' } ] )
759+
760+ const newObj = { id : '2' , type : 'TestObject' }
761+ const count = await cache . invalidateByObject ( newObj )
762+
763+ expect ( count ) . toBe ( 1 )
764+ const cached = await cache . get ( searchKey )
765+ expect ( cached ) . toBeNull ( )
766+ } )
767+
768+ it ( 'should invalidate searchPhrase caches' , async ( ) => {
769+ const searchKey = cache . generateKey ( 'searchPhrase' , { body : { type : 'TestObject' } } )
770+ await cache . set ( searchKey , [ { id : '1' , type : 'TestObject' } ] )
771+
772+ const newObj = { id : '2' , type : 'TestObject' }
773+ const count = await cache . invalidateByObject ( newObj )
774+
775+ expect ( count ) . toBe ( 1 )
776+ const cached = await cache . get ( searchKey )
777+ expect ( cached ) . toBeNull ( )
778+ } )
779+
780+ it ( 'should not invalidate id, history, or since caches' , async ( ) => {
781+ // These caches should not be invalidated by object matching
782+ const idKey = cache . generateKey ( 'id' , '123' )
783+ const historyKey = cache . generateKey ( 'history' , '123' )
784+ const sinceKey = cache . generateKey ( 'since' , '2024-01-01' )
785+
786+ await cache . set ( idKey , { id : '123' , type : 'TestObject' } )
787+ await cache . set ( historyKey , [ { id : '123' } ] )
788+ await cache . set ( sinceKey , [ { id : '123' } ] )
789+
790+ const newObj = { id : '456' , type : 'TestObject' }
791+ const count = await cache . invalidateByObject ( newObj )
792+
793+ // None of these should be invalidated
794+ expect ( await cache . get ( idKey ) ) . toBeTruthy ( )
795+ expect ( await cache . get ( historyKey ) ) . toBeTruthy ( )
796+ expect ( await cache . get ( sinceKey ) ) . toBeTruthy ( )
797+ } )
798+
799+ it ( 'should handle invalid input gracefully' , async ( ) => {
800+ expect ( await cache . invalidateByObject ( null ) ) . toBe ( 0 )
801+ expect ( await cache . invalidateByObject ( undefined ) ) . toBe ( 0 )
802+ expect ( await cache . invalidateByObject ( 'not an object' ) ) . toBe ( 0 )
803+ expect ( await cache . invalidateByObject ( 123 ) ) . toBe ( 0 )
804+ } )
805+
806+ it ( 'should track invalidation count in stats' , async ( ) => {
807+ const queryKey = cache . generateKey ( 'query' , { body : { type : 'TestObject' } } )
808+ await cache . set ( queryKey , [ { id : '1' } ] )
809+
810+ const statsBefore = await cache . getStats ( )
811+ const invalidationsBefore = statsBefore . invalidations
812+
813+ await cache . invalidateByObject ( { type : 'TestObject' } )
814+
815+ const statsAfter = await cache . getStats ( )
816+ expect ( statsAfter . invalidations ) . toBe ( invalidationsBefore + 1 )
817+ } )
818+ } )
819+
820+ describe ( 'objectMatchesQuery' , ( ) => {
821+ it ( 'should match simple property queries' , ( ) => {
822+ const obj = { type : 'TestObject' , name : 'Test' }
823+ expect ( cache . objectMatchesQuery ( obj , { type : 'TestObject' } ) ) . toBe ( true )
824+ expect ( cache . objectMatchesQuery ( obj , { type : 'OtherObject' } ) ) . toBe ( false )
825+ } )
826+
827+ it ( 'should match queries with body property' , ( ) => {
828+ const obj = { type : 'TestObject' }
829+ expect ( cache . objectMatchesQuery ( obj , { body : { type : 'TestObject' } } ) ) . toBe ( true )
830+ expect ( cache . objectMatchesQuery ( obj , { body : { type : 'OtherObject' } } ) ) . toBe ( false )
831+ } )
832+
833+ it ( 'should match nested property queries' , ( ) => {
834+ const obj = { metadata : { author : 'John' } }
835+ expect ( cache . objectMatchesQuery ( obj , { 'metadata.author' : 'John' } ) ) . toBe ( true )
836+ expect ( cache . objectMatchesQuery ( obj , { 'metadata.author' : 'Jane' } ) ) . toBe ( false )
837+ } )
838+ } )
839+
840+ describe ( 'objectContainsProperties' , ( ) => {
841+ it ( 'should skip pagination parameters' , ( ) => {
842+ const obj = { type : 'TestObject' }
843+ expect ( cache . objectContainsProperties ( obj , { type : 'TestObject' , limit : 10 , skip : 5 } ) ) . toBe ( true )
844+ } )
845+
846+ it ( 'should skip __rerum and _id properties' , ( ) => {
847+ const obj = { type : 'TestObject' }
848+ expect ( cache . objectContainsProperties ( obj , { type : 'TestObject' , __rerum : { } , _id : '123' } ) ) . toBe ( true )
849+ } )
850+
851+ it ( 'should match simple properties' , ( ) => {
852+ const obj = { type : 'TestObject' , status : 'active' }
853+ expect ( cache . objectContainsProperties ( obj , { type : 'TestObject' , status : 'active' } ) ) . toBe ( true )
854+ expect ( cache . objectContainsProperties ( obj , { type : 'TestObject' , status : 'inactive' } ) ) . toBe ( false )
855+ } )
856+
857+ it ( 'should match nested objects' , ( ) => {
858+ const obj = { metadata : { author : 'John' , year : 2024 } }
859+ expect ( cache . objectContainsProperties ( obj , { metadata : { author : 'John' , year : 2024 } } ) ) . toBe ( true )
860+ expect ( cache . objectContainsProperties ( obj , { metadata : { author : 'Jane' } } ) ) . toBe ( false )
861+ } )
862+
863+ it ( 'should handle $exists operator' , ( ) => {
864+ const obj = { type : 'TestObject' , optional : 'value' }
865+ expect ( cache . objectContainsProperties ( obj , { optional : { $exists : true } } ) ) . toBe ( true )
866+ expect ( cache . objectContainsProperties ( obj , { missing : { $exists : false } } ) ) . toBe ( true )
867+ expect ( cache . objectContainsProperties ( obj , { type : { $exists : false } } ) ) . toBe ( false )
868+ } )
869+
870+ it ( 'should handle $ne operator' , ( ) => {
871+ const obj = { status : 'active' }
872+ expect ( cache . objectContainsProperties ( obj , { status : { $ne : 'inactive' } } ) ) . toBe ( true )
873+ expect ( cache . objectContainsProperties ( obj , { status : { $ne : 'active' } } ) ) . toBe ( false )
874+ } )
875+
876+ it ( 'should handle comparison operators' , ( ) => {
877+ const obj = { count : 42 }
878+ expect ( cache . objectContainsProperties ( obj , { count : { $gt : 40 } } ) ) . toBe ( true )
879+ expect ( cache . objectContainsProperties ( obj , { count : { $gte : 42 } } ) ) . toBe ( true )
880+ expect ( cache . objectContainsProperties ( obj , { count : { $lt : 50 } } ) ) . toBe ( true )
881+ expect ( cache . objectContainsProperties ( obj , { count : { $lte : 42 } } ) ) . toBe ( true )
882+ expect ( cache . objectContainsProperties ( obj , { count : { $gt : 50 } } ) ) . toBe ( false )
883+ } )
884+
885+ it ( 'should handle $size operator for arrays' , ( ) => {
886+ const obj = { tags : [ 'a' , 'b' , 'c' ] }
887+ expect ( cache . objectContainsProperties ( obj , { tags : { $size : 3 } } ) ) . toBe ( true )
888+ expect ( cache . objectContainsProperties ( obj , { tags : { $size : 2 } } ) ) . toBe ( false )
889+ } )
890+
891+ it ( 'should handle $or operator' , ( ) => {
892+ const obj = { type : 'TestObject' }
893+ expect ( cache . objectContainsProperties ( obj , {
894+ $or : [ { type : 'TestObject' } , { type : 'OtherObject' } ]
895+ } ) ) . toBe ( true )
896+ expect ( cache . objectContainsProperties ( obj , {
897+ $or : [ { type : 'Wrong1' } , { type : 'Wrong2' } ]
898+ } ) ) . toBe ( false )
899+ } )
900+
901+ it ( 'should handle $and operator' , ( ) => {
902+ const obj = { type : 'TestObject' , status : 'active' }
903+ expect ( cache . objectContainsProperties ( obj , {
904+ $and : [ { type : 'TestObject' } , { status : 'active' } ]
905+ } ) ) . toBe ( true )
906+ expect ( cache . objectContainsProperties ( obj , {
907+ $and : [ { type : 'TestObject' } , { status : 'inactive' } ]
908+ } ) ) . toBe ( false )
909+ } )
910+ } )
911+
912+ describe ( 'getNestedProperty' , ( ) => {
913+ it ( 'should get top-level properties' , ( ) => {
914+ const obj = { name : 'Test' }
915+ expect ( cache . getNestedProperty ( obj , 'name' ) ) . toBe ( 'Test' )
916+ } )
917+
918+ it ( 'should get nested properties with dot notation' , ( ) => {
919+ const obj = {
920+ metadata : {
921+ author : {
922+ name : 'John'
923+ }
924+ }
925+ }
926+ expect ( cache . getNestedProperty ( obj , 'metadata.author.name' ) ) . toBe ( 'John' )
927+ } )
928+
929+ it ( 'should return undefined for missing properties' , ( ) => {
930+ const obj = { name : 'Test' }
931+ expect ( cache . getNestedProperty ( obj , 'missing' ) ) . toBeUndefined ( )
932+ expect ( cache . getNestedProperty ( obj , 'missing.nested' ) ) . toBeUndefined ( )
933+ } )
934+
935+ it ( 'should handle null/undefined gracefully' , ( ) => {
936+ const obj = { data : null }
937+ expect ( cache . getNestedProperty ( obj , 'data.nested' ) ) . toBeUndefined ( )
938+ } )
939+ } )
940+
941+ describe ( 'evaluateFieldOperators' , ( ) => {
942+ it ( 'should evaluate $exists correctly' , ( ) => {
943+ expect ( cache . evaluateFieldOperators ( 'value' , { $exists : true } ) ) . toBe ( true )
944+ expect ( cache . evaluateFieldOperators ( undefined , { $exists : false } ) ) . toBe ( true )
945+ expect ( cache . evaluateFieldOperators ( 'value' , { $exists : false } ) ) . toBe ( false )
946+ } )
947+
948+ it ( 'should evaluate $size correctly' , ( ) => {
949+ expect ( cache . evaluateFieldOperators ( [ 1 , 2 , 3 ] , { $size : 3 } ) ) . toBe ( true )
950+ expect ( cache . evaluateFieldOperators ( [ 1 , 2 ] , { $size : 3 } ) ) . toBe ( false )
951+ expect ( cache . evaluateFieldOperators ( 'not array' , { $size : 1 } ) ) . toBe ( false )
952+ } )
953+
954+ it ( 'should evaluate comparison operators correctly' , ( ) => {
955+ expect ( cache . evaluateFieldOperators ( 10 , { $gt : 5 } ) ) . toBe ( true )
956+ expect ( cache . evaluateFieldOperators ( 10 , { $gte : 10 } ) ) . toBe ( true )
957+ expect ( cache . evaluateFieldOperators ( 10 , { $lt : 20 } ) ) . toBe ( true )
958+ expect ( cache . evaluateFieldOperators ( 10 , { $lte : 10 } ) ) . toBe ( true )
959+ expect ( cache . evaluateFieldOperators ( 10 , { $ne : 5 } ) ) . toBe ( true )
960+ } )
961+
962+ it ( 'should be conservative with unknown operators' , ( ) => {
963+ expect ( cache . evaluateFieldOperators ( 'value' , { $unknown : 'test' } ) ) . toBe ( true )
964+ } )
965+ } )
966+
967+ describe ( 'evaluateOperator' , ( ) => {
968+ it ( 'should evaluate $or correctly' , ( ) => {
969+ const obj = { type : 'A' }
970+ expect ( cache . evaluateOperator ( obj , '$or' , [ { type : 'A' } , { type : 'B' } ] ) ) . toBe ( true )
971+ expect ( cache . evaluateOperator ( obj , '$or' , [ { type : 'B' } , { type : 'C' } ] ) ) . toBe ( false )
972+ } )
973+
974+ it ( 'should evaluate $and correctly' , ( ) => {
975+ const obj = { type : 'A' , status : 'active' }
976+ expect ( cache . evaluateOperator ( obj , '$and' , [ { type : 'A' } , { status : 'active' } ] ) ) . toBe ( true )
977+ expect ( cache . evaluateOperator ( obj , '$and' , [ { type : 'A' } , { status : 'inactive' } ] ) ) . toBe ( false )
978+ } )
979+
980+ it ( 'should be conservative with unknown operators' , ( ) => {
981+ const obj = { type : 'A' }
982+ expect ( cache . evaluateOperator ( obj , '$unknown' , 'test' ) ) . toBe ( true )
983+ } )
984+
985+ it ( 'should handle invalid input gracefully' , ( ) => {
986+ const obj = { type : 'A' }
987+ expect ( cache . evaluateOperator ( obj , '$or' , 'not an array' ) ) . toBe ( false )
988+ expect ( cache . evaluateOperator ( obj , '$and' , 'not an array' ) ) . toBe ( false )
989+ } )
990+ } )
991+ } )
0 commit comments