@@ -1011,31 +1011,32 @@ describe(`${Entity.name} denormalization`, () => {
10111011 expect ( result ) . toBeDefined ( ) ;
10121012 if ( ! result ) return ;
10131013
1014- // walk to a depth-limited entity: each hop is 1 entity depth
1015- // dept-0(1) -> bldg-0(2) -> dept-1(3) -> bldg-1(4) -> ...
1016- // At depth 128 we should find truncated entities with unresolved FK ids
1014+ // walk to a depth-limited entity: each dept→bldg→dept step is 2 entity depths
1015+ // dept-k is entered at depth 2k (default MAX_ENTITY_DEPTH is 64)
10171016 let node : any = result ;
1018- for ( let i = 0 ; i < 60 ; i ++ ) {
1017+ for ( let i = 0 ; i < 30 ; i ++ ) {
10191018 expect ( node . buildings ) . toBeDefined ( ) ;
10201019 expect ( node . buildings . length ) . toBe ( 1 ) ;
10211020 node = node . buildings [ 0 ] . departments [ 0 ] ;
10221021 }
10231022 // node should still be a Department instance (well within limit)
10241023 expect ( node ) . toBeInstanceOf ( Department ) ;
10251024
1026- expect ( consoleSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1027- expect ( consoleSpy ) . toHaveBeenCalledWith (
1028- expect . stringContaining ( 'Entity depth limit' ) ,
1029- ) ;
1025+ expect ( consoleSpy ) . toHaveBeenCalled ( ) ;
1026+ expect (
1027+ consoleSpy . mock . calls . some ( args =>
1028+ String ( args [ 0 ] ) . includes ( 'Entity depth limit' ) ,
1029+ ) ,
1030+ ) . toBe ( true ) ;
10301031
10311032 consoleSpy . mockRestore ( ) ;
10321033 } ) ;
10331034
10341035 test ( 'depth-limited entity that is missing from store' , ( ) => {
10351036 const entities = buildChain ( 200 ) ;
1036- // dept-64 is the first entity hit by the depth limit (checked at depth=128 ).
1037+ // dept-32 is the first Department hit by the default depth limit (depth 64 ).
10371038 // Removing it exercises the "entity not found" branch in depthLimitEntity.
1038- delete ( entities . Department as any ) [ 'dept-64 ' ] ;
1039+ delete ( entities . Department as any ) [ 'dept-32 ' ] ;
10391040
10401041 const consoleSpy = jest
10411042 . spyOn ( console , 'error' )
@@ -1073,8 +1074,8 @@ describe(`${Entity.name} denormalization`, () => {
10731074 for ( let i = 0 ; i < 200 ; i ++ ) {
10741075 deptEntities [ `dept-${ i } ` ] = {
10751076 id : `dept-${ i } ` ,
1076- // dept-64 is the first depth-limited entity ; mark it invalid
1077- name : i === 64 ? 'INVALID' : `Department ${ i } ` ,
1077+ // dept-32 is the first depth-limited Department ; mark it invalid
1078+ name : i === 32 ? 'INVALID' : `Department ${ i } ` ,
10781079 buildings : [ `bldg-${ i } ` ] ,
10791080 } ;
10801081 bldgEntities [ `bldg-${ i } ` ] = {
@@ -1099,10 +1100,10 @@ describe(`${Entity.name} denormalization`, () => {
10991100
11001101 test ( 'depth-limited entity with inline object input' , ( ) => {
11011102 const entities = buildChain ( 200 ) ;
1102- // bldg-63 's departments are processed when depth=128 , hitting depthLimitEntity.
1103+ // bldg-31 's departments are processed when depth=64 , hitting depthLimitEntity.
11031104 // Use inline object instead of string pk to exercise the object-input branch.
1104- ( entities . Building as any ) [ 'bldg-63 ' ] . departments = [
1105- { id : 'dept-64 ' , name : 'Inline Department 64 ' , buildings : [ ] } ,
1105+ ( entities . Building as any ) [ 'bldg-31 ' ] . departments = [
1106+ { id : 'dept-32 ' , name : 'Inline Department 32 ' , buildings : [ ] } ,
11061107 ] ;
11071108
11081109 const consoleSpy = jest
@@ -1117,17 +1118,86 @@ describe(`${Entity.name} denormalization`, () => {
11171118
11181119 // walk to the depth-limited inline entity
11191120 let node : any = result ;
1120- for ( let i = 0 ; i < 63 ; i ++ ) {
1121+ for ( let i = 0 ; i < 31 ; i ++ ) {
11211122 node = node . buildings [ 0 ] . departments [ 0 ] ;
11221123 }
1123- // node is dept-63 , its building is bldg-63
1124- expect ( node . id ) . toBe ( 'dept-63 ' ) ;
1124+ // node is dept-31 , its building is bldg-31
1125+ expect ( node . id ) . toBe ( 'dept-31 ' ) ;
11251126 const depthLimitedBldg = node . buildings [ 0 ] ;
1126- expect ( depthLimitedBldg . id ) . toBe ( 'bldg-63 ' ) ;
1127- // dept-64 was provided as an inline object, so depthLimitEntity used it directly
1127+ expect ( depthLimitedBldg . id ) . toBe ( 'bldg-31 ' ) ;
1128+ // dept-32 was provided as an inline object, so depthLimitEntity used it directly
11281129 const inlineDept = depthLimitedBldg . departments [ 0 ] ;
11291130 expect ( inlineDept ) . toBeInstanceOf ( Department ) ;
1130- expect ( inlineDept . id ) . toBe ( 'dept-64' ) ;
1131+ expect ( inlineDept . id ) . toBe ( 'dept-32' ) ;
1132+
1133+ consoleSpy . mockRestore ( ) ;
1134+ } ) ;
1135+
1136+ test ( 'maxEntityDepth on Entity lowers the limit' , ( ) => {
1137+ class LimitedDept extends IDEntity {
1138+ readonly name : string = '' ;
1139+ readonly buildings : LimitedBldg [ ] = [ ] ;
1140+ static maxEntityDepth = 10 ;
1141+ }
1142+ class LimitedBldg extends IDEntity {
1143+ readonly name : string = '' ;
1144+ readonly departments : LimitedDept [ ] = [ ] ;
1145+
1146+ static schema = {
1147+ departments : [ LimitedDept ] ,
1148+ } ;
1149+
1150+ static maxEntityDepth = 10 ;
1151+ }
1152+ LimitedDept . schema = {
1153+ buildings : new schema . Array ( LimitedBldg ) ,
1154+ } ;
1155+
1156+ const deptEntities : Record < string , any > = { } ;
1157+ const bldgEntities : Record < string , any > = { } ;
1158+ for ( let i = 0 ; i < 50 ; i ++ ) {
1159+ deptEntities [ `dept-${ i } ` ] = {
1160+ id : `dept-${ i } ` ,
1161+ name : `Department ${ i } ` ,
1162+ buildings : [ `bldg-${ i } ` ] ,
1163+ } ;
1164+ bldgEntities [ `bldg-${ i } ` ] = {
1165+ id : `bldg-${ i } ` ,
1166+ name : `Building ${ i } ` ,
1167+ departments : i < 49 ? [ `dept-${ i + 1 } ` ] : [ ] ,
1168+ } ;
1169+ }
1170+
1171+ const consoleSpy = jest
1172+ . spyOn ( console , 'error' )
1173+ . mockImplementation ( ( ) => { } ) ;
1174+
1175+ const result = plainDenormalize ( LimitedDept , 'dept-0' , {
1176+ LimitedDept : deptEntities ,
1177+ LimitedBldg : bldgEntities ,
1178+ } ) ;
1179+
1180+ expect ( result ) . not . toEqual ( expect . any ( Symbol ) ) ;
1181+ if ( typeof result === 'symbol' ) return ;
1182+ expect ( result ) . toBeDefined ( ) ;
1183+ if ( ! result ) return ;
1184+
1185+ // depth 10 means 5 full hops (dept→bldg = 2 entity levels per hop)
1186+ // walk 4 hops safely
1187+ let node : any = result ;
1188+ for ( let i = 0 ; i < 4 ; i ++ ) {
1189+ expect ( node . buildings ) . toBeDefined ( ) ;
1190+ expect ( node . buildings . length ) . toBe ( 1 ) ;
1191+ node = node . buildings [ 0 ] . departments [ 0 ] ;
1192+ }
1193+ expect ( node ) . toBeInstanceOf ( LimitedDept ) ;
1194+
1195+ expect ( consoleSpy ) . toHaveBeenCalled ( ) ;
1196+ expect (
1197+ consoleSpy . mock . calls . some ( args =>
1198+ String ( args [ 0 ] ) . includes ( 'Entity depth limit of 10' ) ,
1199+ ) ,
1200+ ) . toBe ( true ) ;
11311201
11321202 consoleSpy . mockRestore ( ) ;
11331203 } ) ;
0 commit comments