@@ -21,7 +21,11 @@ export interface ExpandContext {
2121}
2222
2323export interface ExpansionField {
24- /** Field on the parent object that holds the id (and where we write the hydrated object). */
24+ /**
25+ * Field on the parent object that holds the id (and where we write the hydrated
26+ * object). Dot-separated paths are supported for nested ids
27+ * (e.g. `parent.subscription_details.subscription`).
28+ */
2529 sourcePath : string ;
2630 /** `object` string of the linked resource. Used to recurse via the registry. */
2731 targetObject : string ;
@@ -226,7 +230,7 @@ async function ExpandObjects(
226230 if ( field . embeddedList ) {
227231 const nested : AnyObject [ ] = [ ] ;
228232 for ( const item of items ) {
229- const value = item [ field . sourcePath ] ;
233+ const value = GetByPath ( item , field . sourcePath ) ;
230234 if ( value && typeof value === 'object' ) {
231235 nested . push ( value as AnyObject ) ;
232236 }
@@ -239,7 +243,7 @@ async function ExpandObjects(
239243
240244 const idToParents = new Map < string , AnyObject [ ] > ( ) ;
241245 for ( const item of items ) {
242- const value = item [ field . sourcePath ] ;
246+ const value = GetByPath ( item , field . sourcePath ) ;
243247 if ( typeof value !== 'string' ) continue ;
244248 const parents = idToParents . get ( value ) ?? [ ] ;
245249 parents . push ( item ) ;
@@ -263,7 +267,7 @@ async function ExpandObjects(
263267 for ( const [ id , parents ] of idToParents ) {
264268 const expanded = ctx . cache . get ( CacheKey ( field . targetObject , id ) ) ?? null ;
265269 for ( const parent of parents ) {
266- parent [ field . sourcePath ] = expanded ;
270+ SetByPath ( parent , field . sourcePath , expanded ) ;
267271 }
268272 if ( expanded && tails . length > 0 ) {
269273 nextItems . push ( expanded as AnyObject ) ;
@@ -276,6 +280,35 @@ async function ExpandObjects(
276280 }
277281}
278282
283+ function GetByPath ( obj : AnyObject , path : string ) : unknown {
284+ const parts = path . split ( '.' ) ;
285+ let current : unknown = obj ;
286+ for ( const part of parts ) {
287+ if (
288+ current === null ||
289+ current === undefined ||
290+ typeof current !== 'object'
291+ ) {
292+ return undefined ;
293+ }
294+ current = ( current as AnyObject ) [ part ] ;
295+ }
296+ return current ;
297+ }
298+
299+ function SetByPath ( obj : AnyObject , path : string , value : unknown ) : void {
300+ const parts = path . split ( '.' ) ;
301+ let current : AnyObject = obj ;
302+ for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
303+ const next = current [ parts [ i ] ] ;
304+ if ( next === null || next === undefined || typeof next !== 'object' ) {
305+ return ;
306+ }
307+ current = next as AnyObject ;
308+ }
309+ current [ parts [ parts . length - 1 ] ] = value ;
310+ }
311+
279312function CacheKey ( objectType : string , id : string ) : string {
280313 return `${ objectType } :${ id } ` ;
281314}
0 commit comments