@@ -325,17 +325,9 @@ function matchRecursive(value: unknown, path: Path, currentPath: SingleValuePath
325
325
const endIndex = end === '' ? value . length : end
326
326
327
327
// We'll accumulate all matches from each index in the range
328
- let results : MatchEntry [ ] = [ ]
329
-
330
- // Decide whether the range is exclusive or inclusive. The example in
331
- // the doc says "array[1:9]" => element 1 through 9 (non-inclusive?).
332
- // Typically, in slice terms, that is `array.slice(1, 9)` → includes
333
- // indices 1..8. If that's your intention, do i < endIndex.
334
- for ( let i = startIndex ; i < endIndex ; i ++ ) {
335
- results = results . concat ( matchRecursive ( value [ i ] , rest , [ ...currentPath , i ] ) )
336
- }
337
-
338
- return results
328
+ return value
329
+ . slice ( startIndex , endIndex )
330
+ . flatMap ( ( item , i ) => matchRecursive ( item , rest , [ ...currentPath , i + startIndex ] ) )
339
331
}
340
332
341
333
// 4) Keyed segment => find index in array
@@ -347,7 +339,7 @@ function matchRecursive(value: unknown, path: Path, currentPath: SingleValuePath
347
339
}
348
340
349
341
const nextVal = value [ arrIndex ]
350
- return matchRecursive ( nextVal , rest , [ ...currentPath , arrIndex ] )
342
+ return matchRecursive ( nextVal , rest , [ ...currentPath , { _key : keyed . _key } ] )
351
343
}
352
344
353
345
// this is a similar array key to the studio:
@@ -497,9 +489,6 @@ export function unset(input: unknown, pathExpressions: string[]): unknown {
497
489
return ensureArrayKeysDeep ( result )
498
490
}
499
491
500
- const operations = [ 'before' , 'after' , 'replace' ] as const
501
- type Operation = ( typeof operations ) [ number ]
502
-
503
492
/**
504
493
* Given an input object, a path expression (inside the insert patch object), and an array of items,
505
494
* this function will insert or replace the matched items.
@@ -579,105 +568,135 @@ type Operation = (typeof operations)[number]
579
568
* ```
580
569
*/
581
570
export function insert < R > ( input : unknown , insertPatch : InsertPatch ) : R
582
- export function insert ( input : unknown , insertPatch : InsertPatch ) : unknown {
583
- const operation = operations . find ( ( op ) => op in insertPatch )
571
+ export function insert ( input : unknown , { items, ...insertPatch } : InsertPatch ) : unknown {
572
+ let operation
573
+ let pathExpression
574
+
575
+ // behavior observed from content-lake when inserting:
576
+ // 1. if the operation is before, out of all the matches, it will use the
577
+ // insert the items before the first match that appears in the array
578
+ // 2. if the operation is after, it will insert the items after the first
579
+ // match that appears in the array
580
+ // 3. if the operation is replace, then insert the items before the first
581
+ // match and then delete the rest
582
+ if ( 'before' in insertPatch ) {
583
+ operation = 'before' as const
584
+ pathExpression = insertPatch . before
585
+ } else if ( 'after' in insertPatch ) {
586
+ operation = 'after' as const
587
+ pathExpression = insertPatch . after
588
+ } else if ( 'replace' in insertPatch ) {
589
+ operation = 'replace' as const
590
+ pathExpression = insertPatch . replace
591
+ }
584
592
if ( ! operation ) return input
585
-
586
- const { items} = insertPatch
587
- const pathExpression = ( insertPatch as { [ K in Operation ] ?: string } & { items : unknown } ) [ operation ]
588
593
if ( typeof pathExpression !== 'string' ) return input
589
594
590
- // Helper to normalize a matched index given the parent array's length.
591
- function normalizeIndex ( index : number , parentLength : number ) : number {
592
- switch ( operation ) {
593
- case 'before' :
594
- // A negative index means "append" (i.e. insert before a hypothetical element
595
- // beyond the end of the array).
596
- return index < 0 ? parentLength : index
597
- case 'after' :
598
- // For "after", if the matched index is negative, we treat it as "prepend":
599
- // by convention, we convert it to -1 so that later adding 1 produces 0.
600
- return index < 0 ? - 1 : index
601
- default : // default to 'replace'
602
- // For replace, convert a negative index to the corresponding positive one.
603
- return index < 0 ? parentLength + index : index
604
- }
605
- }
595
+ const parsedPath = parsePath ( pathExpression )
596
+ // in order to do an insert patch, you need to provide at least one path segment
597
+ if ( ! parsedPath . length ) return input
606
598
607
- // Group the matched array entries by their parent array.
608
- interface GroupEntry {
609
- array : unknown [ ]
610
- pathToArray : SingleValuePath
611
- indexes : number [ ]
612
- }
613
- const grouped = new Map < unknown , GroupEntry > ( )
614
- jsonMatch ( input , pathExpression )
615
- . map ( ( { path} ) => {
616
- const segment = path [ path . length - 1 ]
617
- let index : number | undefined
618
- if ( isKeySegment ( segment ) ) {
619
- index = getIndexForKey ( input , segment . _key )
620
- } else if ( typeof segment === 'number' ) {
621
- index = segment
622
- }
623
- if ( typeof index !== 'number' ) return null
599
+ const arrayPath = stringifyPath ( parsedPath . slice ( 0 , - 1 ) )
600
+ const positionPath = stringifyPath ( parsedPath . slice ( - 1 ) )
624
601
625
- const parentPath = path . slice ( 0 , path . length - 1 )
626
- const parent = getDeep ( input , parentPath )
627
- if ( ! Array . isArray ( parent ) ) return null
602
+ const arrayMatches = jsonMatch < unknown [ ] > ( input , arrayPath )
628
603
629
- const normalizedIndex = normalizeIndex ( index , parent . length )
630
- return { parent, parentPath, normalizedIndex}
631
- } )
632
- . filter ( isNonNullable )
633
- . forEach ( ( { parent, parentPath, normalizedIndex} ) => {
634
- if ( grouped . has ( parent ) ) {
635
- grouped . get ( parent ) ! . indexes . push ( normalizedIndex )
636
- } else {
637
- grouped . set ( parent , { array : parent , pathToArray : parentPath , indexes : [ normalizedIndex ] } )
638
- }
639
- } )
604
+ let result = input
640
605
641
- // Sort the indexes for each grouped entry.
642
- const groupEntries = Array . from ( grouped . values ( ) ) . map ( ( entry ) => ( {
643
- ...entry ,
644
- indexes : entry . indexes . sort ( ( a , b ) => a - b ) ,
645
- } ) )
606
+ for ( const { path, value} of arrayMatches ) {
607
+ if ( ! Array . isArray ( value ) ) continue
608
+ let arr = value
646
609
647
- // For each group, update the parent array using setDeep.
648
- const result = groupEntries . reduce < unknown > ( ( acc , { array, indexes, pathToArray} ) => {
649
610
switch ( operation ) {
611
+ case 'replace' : {
612
+ const indexesToRemove = new Set < number > ( )
613
+ let position = Infinity
614
+
615
+ for ( const itemMatch of jsonMatch ( arr , positionPath ) ) {
616
+ // there should only be one path segment for an insert patch, invalid otherwise
617
+ if ( itemMatch . path . length !== 1 ) continue
618
+ const [ segment ] = itemMatch . path
619
+ if ( typeof segment === 'string' ) continue
620
+
621
+ let index
622
+
623
+ if ( typeof segment === 'number' ) index = segment
624
+ if ( isKeySegment ( segment ) ) index = getIndexForKey ( arr , segment . _key )
625
+ if ( typeof index !== 'number' ) continue
626
+ if ( index < 0 ) index = arr . length + index
627
+
628
+ indexesToRemove . add ( index )
629
+ if ( index < position ) position = index
630
+ }
631
+
632
+ if ( position === Infinity ) continue
633
+
634
+ // remove all other indexes
635
+ arr = arr
636
+ . map ( ( item , index ) => ( { item, index} ) )
637
+ . filter ( ( { index} ) => ! indexesToRemove . has ( index ) )
638
+ . map ( ( { item} ) => item )
639
+
640
+ // insert at the min index
641
+ arr = [ ...arr . slice ( 0 , position ) , ...items , ...arr . slice ( position , arr . length ) ]
642
+
643
+ break
644
+ }
650
645
case 'before' : {
651
- // Insert items before the first matched index.
652
- const firstIndex = indexes [ 0 ]
653
- return setDeep ( acc , pathToArray , [
654
- ...array . slice ( 0 , firstIndex ) ,
655
- ...items ,
656
- ...array . slice ( firstIndex ) ,
657
- ] )
646
+ let position = Infinity
647
+
648
+ for ( const itemMatch of jsonMatch ( arr , positionPath ) ) {
649
+ if ( itemMatch . path . length !== 1 ) continue
650
+ const [ segment ] = itemMatch . path
651
+
652
+ if ( typeof segment === 'string' ) continue
653
+
654
+ let index
655
+
656
+ if ( typeof segment === 'number' ) index = segment
657
+ if ( isKeySegment ( segment ) ) index = getIndexForKey ( arr , segment . _key )
658
+ if ( typeof index !== 'number' ) continue
659
+ if ( index < 0 ) index = arr . length - index
660
+ if ( index < position ) position = index
661
+ }
662
+
663
+ if ( position === Infinity ) continue
664
+
665
+ arr = [ ...arr . slice ( 0 , position ) , ...items , ...arr . slice ( position , arr . length ) ]
666
+
667
+ break
658
668
}
659
669
case 'after' : {
660
- // Insert items after the last matched index.
661
- const lastIndex = indexes [ indexes . length - 1 ] + 1
662
- return setDeep ( acc , pathToArray , [
663
- ...array . slice ( 0 , lastIndex ) ,
664
- ...items ,
665
- ...array . slice ( lastIndex ) ,
666
- ] )
670
+ let position = - Infinity
671
+
672
+ for ( const itemMatch of jsonMatch ( arr , positionPath ) ) {
673
+ if ( itemMatch . path . length !== 1 ) continue
674
+ const [ segment ] = itemMatch . path
675
+
676
+ if ( typeof segment === 'string' ) continue
677
+
678
+ let index
679
+
680
+ if ( typeof segment === 'number' ) index = segment
681
+ if ( isKeySegment ( segment ) ) index = getIndexForKey ( arr , segment . _key )
682
+ if ( typeof index !== 'number' ) continue
683
+ if ( index > position ) position = index
684
+ }
685
+
686
+ if ( position === - Infinity ) continue
687
+
688
+ arr = [ ...arr . slice ( 0 , position + 1 ) , ...items , ...arr . slice ( position + 1 , arr . length ) ]
689
+
690
+ break
667
691
}
668
- // default to 'replace' behavior
692
+
669
693
default : {
670
- // Remove all matched items then insert the new items at the first match.
671
- const firstIndex = indexes [ 0 ]
672
- const indexSet = new Set ( indexes )
673
- return setDeep ( acc , pathToArray , [
674
- ...array . slice ( 0 , firstIndex ) ,
675
- ...items ,
676
- ...array . slice ( firstIndex ) . filter ( ( _ , idx ) => ! indexSet . has ( idx + firstIndex ) ) ,
677
- ] )
694
+ continue
678
695
}
679
696
}
680
- } , input )
697
+
698
+ result = setDeep ( result , path , arr )
699
+ }
681
700
682
701
return ensureArrayKeysDeep ( result )
683
702
}
@@ -807,10 +826,6 @@ export function ifRevisionID(input: unknown, revisionId: string): unknown {
807
826
return input
808
827
}
809
828
810
- function isNonNullable < T > ( t : T ) : t is NonNullable < T > {
811
- return t !== null && t !== undefined
812
- }
813
-
814
829
const indexCache = new WeakMap < KeyedSegment [ ] , Record < string , number | undefined > > ( )
815
830
export function getIndexForKey ( input : unknown , key : string ) : number | undefined {
816
831
if ( ! Array . isArray ( input ) ) return undefined
0 commit comments