assign-gingerly 0.0.78 → 0.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DX/paths.js +15 -10
- package/DX/paths.ts +96 -90
- package/assignGingerly.js +171 -76
- package/assignGingerly.ts +200 -78
- package/assignPermissions/PermissionProcessor.js +34 -7
- package/assignPermissions/PermissionProcessor.ts +45 -10
- package/evaluatePathWithAsyncMethods.js +6 -4
- package/evaluatePathWithAsyncMethods.ts +6 -4
- package/handleIshProperty.js +1 -1
- package/handleIshProperty.ts +1 -1
- package/handlers/manageTemplateList.js +1 -1
- package/handlers/manageTemplateList.ts +1 -1
- package/index.js +1 -1
- package/index.ts +1 -1
- package/inferencer/types/NewCustomElement.md +180 -5
- package/inferencer/types/assign-gingerly/types.d.ts +5 -1
- package/inferencer/types/face-up/types.d.ts +5 -0
- package/inferencer/types/roundabout/types.d.ts +1 -0
- package/object-extension.js +1 -1
- package/object-extension.ts +1 -1
- package/package.json +7 -6
- package/types/assign-gingerly/types.d.ts +5 -1
- /package/{waitForEvent.js → utils/waitForEvent.js} +0 -0
- /package/{waitForEvent.ts → utils/waitForEvent.ts} +0 -0
- /package/{waitForSettled.js → utils/waitForSettled.js} +0 -0
- /package/{waitForSettled.ts → utils/waitForSettled.ts} +0 -0
package/assignGingerly.ts
CHANGED
|
@@ -506,9 +506,10 @@ export function evaluatePathWithMethods(
|
|
|
506
506
|
value: any,
|
|
507
507
|
withMethods: Set<string>,
|
|
508
508
|
permissionProcessor?: PermissionProcessor
|
|
509
|
-
): { target: any; lastKey: string; isMethod: boolean; isZeroArg: boolean } {
|
|
509
|
+
): { target: any; lastKey: string; isMethod: boolean; isZeroArg: boolean; lastSegmentConsumed: boolean } {
|
|
510
510
|
let current = target;
|
|
511
511
|
let i = 0;
|
|
512
|
+
let lastSegmentConsumed = false;
|
|
512
513
|
|
|
513
514
|
// Process all segments except the last one
|
|
514
515
|
while (i < pathParts.length - 1) {
|
|
@@ -525,13 +526,17 @@ export function evaluatePathWithMethods(
|
|
|
525
526
|
const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
|
|
526
527
|
const method = current[methodName];
|
|
527
528
|
if (typeof method === 'function') {
|
|
529
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
528
530
|
if (isZeroArgMethod || nextIsMethod) {
|
|
529
531
|
// Zero-arg call - next part is either a method or explicitly not an argument
|
|
530
|
-
current = method.call(current);
|
|
532
|
+
current = method.call(current, ...appendArgs);
|
|
531
533
|
} else {
|
|
532
534
|
// Only current is method - call with next part as string arg
|
|
533
|
-
current = method.call(current, nextPart);
|
|
535
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
534
536
|
i++; // Skip next part since we consumed it as argument
|
|
537
|
+
if (i === pathParts.length - 1) {
|
|
538
|
+
lastSegmentConsumed = true;
|
|
539
|
+
}
|
|
535
540
|
}
|
|
536
541
|
} else {
|
|
537
542
|
// Not a function - just access property (create if needed)
|
|
@@ -560,7 +565,8 @@ export function evaluatePathWithMethods(
|
|
|
560
565
|
target: current,
|
|
561
566
|
lastKey,
|
|
562
567
|
isMethod: isAllowedMethod(lastKey, withMethods, permissionProcessor),
|
|
563
|
-
isZeroArg
|
|
568
|
+
isZeroArg,
|
|
569
|
+
lastSegmentConsumed
|
|
564
570
|
};
|
|
565
571
|
}
|
|
566
572
|
|
|
@@ -649,8 +655,9 @@ function applyToEach(
|
|
|
649
655
|
const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
|
|
650
656
|
const method = current[methodName];
|
|
651
657
|
if (typeof method === 'function') {
|
|
658
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
652
659
|
if (isZeroArgMethod) {
|
|
653
|
-
current = method.call(current);
|
|
660
|
+
current = method.call(current, ...appendArgs);
|
|
654
661
|
continue;
|
|
655
662
|
}
|
|
656
663
|
// For methods in the middle, we need to check the next part
|
|
@@ -659,13 +666,13 @@ function applyToEach(
|
|
|
659
666
|
const nextIsMethod = nextPart && (withMethods.has(nextPart)
|
|
660
667
|
|| (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
|
|
661
668
|
if (nextIsMethod) {
|
|
662
|
-
current = method.call(current);
|
|
669
|
+
current = method.call(current, ...appendArgs);
|
|
663
670
|
} else if (nextPart) {
|
|
664
|
-
current = method.call(current, nextPart);
|
|
671
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
665
672
|
// Skip next part
|
|
666
673
|
pathToForEach.splice(nextIndex, 1);
|
|
667
674
|
} else {
|
|
668
|
-
current = method.call(current);
|
|
675
|
+
current = method.call(current, ...appendArgs);
|
|
669
676
|
}
|
|
670
677
|
} else {
|
|
671
678
|
current = current[methodName];
|
|
@@ -687,13 +694,14 @@ function applyToEach(
|
|
|
687
694
|
// Last segment is a method - call it
|
|
688
695
|
const method = result.target[result.lastKey];
|
|
689
696
|
if (typeof method === 'function') {
|
|
697
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
690
698
|
if (result.isZeroArg) {
|
|
691
|
-
// Trailing | marker - call with
|
|
692
|
-
method.call(result.target);
|
|
699
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
700
|
+
method.call(result.target, ...appendArgs);
|
|
693
701
|
} else if (Array.isArray(value)) {
|
|
694
|
-
method.apply(result.target, value);
|
|
702
|
+
method.apply(result.target, [...value, ...appendArgs]);
|
|
695
703
|
} else {
|
|
696
|
-
method.call(result.target, value);
|
|
704
|
+
method.call(result.target, value, ...appendArgs);
|
|
697
705
|
}
|
|
698
706
|
}
|
|
699
707
|
} else {
|
|
@@ -721,6 +729,110 @@ function applyToEach(
|
|
|
721
729
|
}
|
|
722
730
|
}
|
|
723
731
|
|
|
732
|
+
/**
|
|
733
|
+
* Resolve the RHS of a toggle (=!) command to the value that should be negated.
|
|
734
|
+
* Supports:
|
|
735
|
+
* - '?.path' nested path strings resolved against target (returns the resolved value, or true if missing)
|
|
736
|
+
* - plain key strings looked up on target (returns the value, or true if missing)
|
|
737
|
+
* - resolved literal values (e.g., booleans from assignFrom) returned as-is
|
|
738
|
+
*/
|
|
739
|
+
function resolveValueToNegate(rhsPath: any, target: any): any {
|
|
740
|
+
if (typeof rhsPath === 'string' && isNestedPath(rhsPath)) {
|
|
741
|
+
const rhsPathParts = parsePath(rhsPath);
|
|
742
|
+
let current = target;
|
|
743
|
+
let exists = true;
|
|
744
|
+
for (const part of rhsPathParts) {
|
|
745
|
+
if (current && typeof current === 'object' && part in current) {
|
|
746
|
+
current = current[part];
|
|
747
|
+
} else {
|
|
748
|
+
exists = false;
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return exists ? current : true;
|
|
753
|
+
}
|
|
754
|
+
if (typeof rhsPath === 'string') {
|
|
755
|
+
return (rhsPath in target) ? target[rhsPath] : true;
|
|
756
|
+
}
|
|
757
|
+
// Non-string resolved value (e.g., boolean, number, null from assignFrom)
|
|
758
|
+
return rhsPath;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Navigate a path that leads to an iterable, returning the iterable value or undefined.
|
|
763
|
+
* Handles withMethods and the evaluatePathWithMethods semantics.
|
|
764
|
+
*/
|
|
765
|
+
function getIterableAtPath(
|
|
766
|
+
target: any,
|
|
767
|
+
pathParts: string[],
|
|
768
|
+
value: any,
|
|
769
|
+
withMethods: Set<string> | undefined,
|
|
770
|
+
permissionProcessor?: PermissionProcessor
|
|
771
|
+
): any {
|
|
772
|
+
let current = target;
|
|
773
|
+
if (pathParts.length > 0) {
|
|
774
|
+
if (withMethods && withMethods.size > 0) {
|
|
775
|
+
const result = evaluatePathWithMethods(target, pathParts, value, withMethods, permissionProcessor);
|
|
776
|
+
// evaluatePathWithMethods returns the container object + last key by default.
|
|
777
|
+
// If the last segment was consumed as a method argument, or the last segment
|
|
778
|
+
// is a zero-arg method marked with |, result.target is already the value.
|
|
779
|
+
if (result.lastSegmentConsumed) {
|
|
780
|
+
current = result.target;
|
|
781
|
+
} else if (result.isMethod) {
|
|
782
|
+
const method = result.target[result.lastKey];
|
|
783
|
+
if (typeof method === 'function') {
|
|
784
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
785
|
+
current = method.call(result.target, ...appendArgs);
|
|
786
|
+
} else {
|
|
787
|
+
current = method;
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
current = result.target[result.lastKey];
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
for (const part of pathParts) {
|
|
794
|
+
current = current[part];
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return isIterable(current) ? current : undefined;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Apply an operator command (+=, =!, -=, Y=) to each item in an iterable.
|
|
803
|
+
* Detects @each in the path, navigates to the iterable, then builds a synthetic
|
|
804
|
+
* command key for the remaining path and delegates to assignGingerly per item.
|
|
805
|
+
* Nested @each is handled recursively through assignGingerly.
|
|
806
|
+
*/
|
|
807
|
+
function applyCommandToEach(
|
|
808
|
+
target: any,
|
|
809
|
+
pathParts: string[],
|
|
810
|
+
commandSuffix: string,
|
|
811
|
+
value: any,
|
|
812
|
+
withMethods: Set<string> | undefined,
|
|
813
|
+
aliasMap: Map<string, string>,
|
|
814
|
+
options?: IAssignGingerlyOptions,
|
|
815
|
+
permissionProcessor?: PermissionProcessor
|
|
816
|
+
): void {
|
|
817
|
+
const forEachIndex = pathParts.findIndex(part => isForEachSymbol(part, aliasMap));
|
|
818
|
+
if (forEachIndex === -1) return;
|
|
819
|
+
|
|
820
|
+
const pathToForEach = pathParts.slice(0, forEachIndex);
|
|
821
|
+
const pathAfterForEach = pathParts.slice(forEachIndex + 1);
|
|
822
|
+
|
|
823
|
+
const iterable = getIterableAtPath(target, pathToForEach, value, withMethods, permissionProcessor);
|
|
824
|
+
if (!iterable) return;
|
|
825
|
+
|
|
826
|
+
const items = Array.isArray(iterable) ? iterable : Array.from(iterable);
|
|
827
|
+
const syntheticKey = pathAfterForEach.length > 0
|
|
828
|
+
? `?.${pathAfterForEach.join('?.')}${commandSuffix}`
|
|
829
|
+
: commandSuffix;
|
|
830
|
+
|
|
831
|
+
for (const item of items) {
|
|
832
|
+
assignGingerly(item, { [syntheticKey]: value }, options, permissionProcessor);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
724
836
|
/**
|
|
725
837
|
* Apply alias substitutions to a key string.
|
|
726
838
|
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
@@ -842,6 +954,12 @@ export function assignGingerly(
|
|
|
842
954
|
if (isNestedPath(path)) {
|
|
843
955
|
const pathParts = parsePath(path);
|
|
844
956
|
|
|
957
|
+
// Check for @each in path
|
|
958
|
+
if (pathParts.some(part => isForEachSymbol(part, aliasMap))) {
|
|
959
|
+
applyCommandToEach(target, pathParts, ' +=', value, withMethodsSet, aliasMap, options, permissionProcessor);
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
|
|
845
963
|
// Check for withMethods path evaluation
|
|
846
964
|
let lhsValue: any;
|
|
847
965
|
let lhsParent: any;
|
|
@@ -912,52 +1030,51 @@ export function assignGingerly(
|
|
|
912
1030
|
const lhsPath = parseToggleCommand(key);
|
|
913
1031
|
if (lhsPath) {
|
|
914
1032
|
const rhsPath = value;
|
|
915
|
-
|
|
916
|
-
// Resolve LHS
|
|
917
|
-
let lhsParent: any;
|
|
918
|
-
let lhsLastKey: string;
|
|
1033
|
+
|
|
919
1034
|
if (isNestedPath(lhsPath)) {
|
|
920
1035
|
const lhsPathParts = parsePath(lhsPath);
|
|
921
|
-
lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
|
|
922
|
-
lhsParent = ensureNestedPath(target, lhsPathParts);
|
|
923
|
-
} else {
|
|
924
|
-
lhsLastKey = lhsPath;
|
|
925
|
-
lhsParent = target;
|
|
926
|
-
}
|
|
927
1036
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1037
|
+
// Check for @each in the LHS path
|
|
1038
|
+
if (lhsPathParts.some(part => isForEachSymbol(part, aliasMap))) {
|
|
1039
|
+
// Resolve non-self-referencing RHS paths against the original target
|
|
1040
|
+
// before iterating, so each item negates the same root value.
|
|
1041
|
+
const resolvedValue = (rhsPath === '.' || typeof rhsPath !== 'string')
|
|
1042
|
+
? rhsPath
|
|
1043
|
+
: resolveValueToNegate(rhsPath, target);
|
|
1044
|
+
applyCommandToEach(target, lhsPathParts, ' =!', resolvedValue, withMethodsSet, aliasMap, options, permissionProcessor);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// No @each in path - standard toggle
|
|
1049
|
+
const lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
|
|
1050
|
+
const lhsParent = ensureNestedPath(target, lhsPathParts);
|
|
1051
|
+
|
|
1052
|
+
// Determine what to negate
|
|
1053
|
+
let valueToNegate: any;
|
|
1054
|
+
if (rhsPath === '.') {
|
|
1055
|
+
valueToNegate = (lhsLastKey in lhsParent) ? lhsParent[lhsLastKey] : undefined;
|
|
934
1056
|
} else {
|
|
935
|
-
valueToNegate =
|
|
1057
|
+
valueToNegate = resolveValueToNegate(rhsPath, target);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
|
|
1061
|
+
lhsParent[lhsLastKey] = !valueToNegate;
|
|
936
1062
|
}
|
|
937
1063
|
} else {
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
current = current[part];
|
|
946
|
-
} else {
|
|
947
|
-
exists = false;
|
|
948
|
-
break;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
valueToNegate = exists ? current : true;
|
|
1064
|
+
// Plain key LHS
|
|
1065
|
+
const lhsLastKey = lhsPath;
|
|
1066
|
+
const lhsParent = target;
|
|
1067
|
+
|
|
1068
|
+
let valueToNegate: any;
|
|
1069
|
+
if (rhsPath === '.') {
|
|
1070
|
+
valueToNegate = (lhsLastKey in lhsParent) ? lhsParent[lhsLastKey] : undefined;
|
|
952
1071
|
} else {
|
|
953
|
-
|
|
954
|
-
|
|
1072
|
+
valueToNegate = resolveValueToNegate(rhsPath, target);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
|
|
1076
|
+
lhsParent[lhsLastKey] = !valueToNegate;
|
|
955
1077
|
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Apply negation to LHS — check restriction first
|
|
959
|
-
if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
|
|
960
|
-
lhsParent[lhsLastKey] = !valueToNegate;
|
|
961
1078
|
}
|
|
962
1079
|
}
|
|
963
1080
|
continue;
|
|
@@ -973,6 +1090,13 @@ export function assignGingerly(
|
|
|
973
1090
|
|
|
974
1091
|
if (isNestedPath(path)) {
|
|
975
1092
|
const pathParts = parsePath(path);
|
|
1093
|
+
|
|
1094
|
+
// Check for @each in path
|
|
1095
|
+
if (pathParts.some(part => isForEachSymbol(part, aliasMap))) {
|
|
1096
|
+
applyCommandToEach(target, pathParts, ' -=', value, withMethodsSet, aliasMap, options, permissionProcessor);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
976
1100
|
if (pathParts.length === 0) {
|
|
977
1101
|
parent = target;
|
|
978
1102
|
} else {
|
|
@@ -1016,6 +1140,13 @@ export function assignGingerly(
|
|
|
1016
1140
|
if (permissionProcessor?.checkRestrictedProp(lastKey)) {
|
|
1017
1141
|
continue;
|
|
1018
1142
|
}
|
|
1143
|
+
|
|
1144
|
+
// Check for @each in path
|
|
1145
|
+
if (isNestedPath(path) && pathParts.some(part => isForEachSymbol(part, aliasMap))) {
|
|
1146
|
+
applyCommandToEach(target, pathParts, ' Y=', value, withMethodsSet, aliasMap, options, permissionProcessor);
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1019
1150
|
// Navigate to the target sub-object
|
|
1020
1151
|
let mergeTarget: any;
|
|
1021
1152
|
if (isNestedPath(path)) {
|
|
@@ -1083,24 +1214,12 @@ export function assignGingerly(
|
|
|
1083
1214
|
// Static forEach (@each) - existing logic
|
|
1084
1215
|
const pathToForEach = pathParts.slice(0, forEachIndex);
|
|
1085
1216
|
const pathAfterForEach = pathParts.slice(forEachIndex + 1);
|
|
1086
|
-
|
|
1217
|
+
|
|
1087
1218
|
// Navigate to the iterable
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
if (withMethodsSet) {
|
|
1091
|
-
const result = evaluatePathWithMethods(target, pathToForEach, value, withMethodsSet, permissionProcessor);
|
|
1092
|
-
// The result.target is the current position after evaluating the path
|
|
1093
|
-
// This is already the iterable we want
|
|
1094
|
-
current = result.target;
|
|
1095
|
-
} else {
|
|
1096
|
-
for (const part of pathToForEach) {
|
|
1097
|
-
current = current[part];
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1219
|
+
const current = getIterableAtPath(target, pathToForEach, value, withMethodsSet, permissionProcessor);
|
|
1220
|
+
|
|
1102
1221
|
// Apply to each item in the iterable
|
|
1103
|
-
if (
|
|
1222
|
+
if (current) {
|
|
1104
1223
|
applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options, permissionProcessor);
|
|
1105
1224
|
}
|
|
1106
1225
|
// If not iterable, let JavaScript throw error naturally when trying to iterate
|
|
@@ -1130,12 +1249,13 @@ export function assignGingerly(
|
|
|
1130
1249
|
// Last segment is a method — call it
|
|
1131
1250
|
const method = result.target[result.lastKey];
|
|
1132
1251
|
if (typeof method === 'function') {
|
|
1133
|
-
|
|
1252
|
+
const appendArgs = capturedPermissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
1253
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
1134
1254
|
const returnVal = result.isZeroArg
|
|
1135
|
-
? method.call(result.target)
|
|
1255
|
+
? method.call(result.target, ...appendArgs)
|
|
1136
1256
|
: Array.isArray(capturedValue)
|
|
1137
|
-
? method.apply(result.target, capturedValue)
|
|
1138
|
-
: method.call(result.target, capturedValue);
|
|
1257
|
+
? method.apply(result.target, [...capturedValue, ...appendArgs])
|
|
1258
|
+
: method.call(result.target, capturedValue, ...appendArgs);
|
|
1139
1259
|
// If it's an async method, await it (for side effects)
|
|
1140
1260
|
if (result.isAsyncMethod) await returnVal;
|
|
1141
1261
|
}
|
|
@@ -1171,13 +1291,14 @@ export function assignGingerly(
|
|
|
1171
1291
|
// Last segment is a method - call it
|
|
1172
1292
|
const method = result.target[result.lastKey];
|
|
1173
1293
|
if (typeof method === 'function') {
|
|
1294
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
|
|
1174
1295
|
if (result.isZeroArg) {
|
|
1175
|
-
// Trailing | marker - call with
|
|
1176
|
-
method.call(result.target);
|
|
1296
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
1297
|
+
method.call(result.target, ...appendArgs);
|
|
1177
1298
|
} else if (Array.isArray(value)) {
|
|
1178
|
-
method.apply(result.target, value);
|
|
1299
|
+
method.apply(result.target, [...value, ...appendArgs]);
|
|
1179
1300
|
} else {
|
|
1180
|
-
method.call(result.target, value);
|
|
1301
|
+
method.call(result.target, value, ...appendArgs);
|
|
1181
1302
|
}
|
|
1182
1303
|
}
|
|
1183
1304
|
// Silently skip if not a function
|
|
@@ -1245,13 +1366,14 @@ export function assignGingerly(
|
|
|
1245
1366
|
const methodName = isZeroArgKey ? key.slice(0, -1) : key;
|
|
1246
1367
|
const method = target[methodName];
|
|
1247
1368
|
if (typeof method === 'function') {
|
|
1369
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
|
|
1248
1370
|
if (isZeroArgKey) {
|
|
1249
|
-
// Trailing | marker - call with
|
|
1250
|
-
method.call(target);
|
|
1371
|
+
// Trailing | marker - call with appended args only, ignoring the value
|
|
1372
|
+
method.call(target, ...appendArgs);
|
|
1251
1373
|
} else if (Array.isArray(value)) {
|
|
1252
|
-
method.apply(target, value);
|
|
1374
|
+
method.apply(target, [...value, ...appendArgs]);
|
|
1253
1375
|
} else {
|
|
1254
|
-
method.call(target, value);
|
|
1376
|
+
method.call(target, value, ...appendArgs);
|
|
1255
1377
|
}
|
|
1256
1378
|
}
|
|
1257
1379
|
// Silently skip if not a function
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { isAllowedUrl } from './isAllowedUrl.js';
|
|
2
|
+
import { getValue } from '../resolve/getValues.js';
|
|
2
3
|
export class PermissionProcessor {
|
|
3
4
|
constructor(permissions) {
|
|
4
5
|
this.permissions = permissions;
|
|
5
6
|
const { props, attrs } = buildMaps(permissions);
|
|
6
7
|
this.props = props;
|
|
7
8
|
this.attrs = attrs;
|
|
8
|
-
|
|
9
|
+
const { blockedMethods, configuredMethods } = buildMethodMaps(permissions);
|
|
10
|
+
this.blockedMethods = blockedMethods;
|
|
11
|
+
this.configuredMethods = configuredMethods;
|
|
9
12
|
this.warned = new Set();
|
|
10
13
|
this.warnedMethods = new Set();
|
|
11
14
|
}
|
|
@@ -25,11 +28,29 @@ export class PermissionProcessor {
|
|
|
25
28
|
return true;
|
|
26
29
|
}
|
|
27
30
|
checkRestrictedMethod(methodName) {
|
|
28
|
-
if (!this.
|
|
31
|
+
if (!this.blockedMethods.has(methodName))
|
|
29
32
|
return false;
|
|
30
33
|
this.warnRestrictedMethod(methodName);
|
|
31
34
|
return true;
|
|
32
35
|
}
|
|
36
|
+
getMethodAppendArgs(methodName) {
|
|
37
|
+
const config = this.configuredMethods.get(methodName);
|
|
38
|
+
if (!config)
|
|
39
|
+
return undefined;
|
|
40
|
+
const rawArgs = config.appendArgs ?? config.addArgs;
|
|
41
|
+
if (!rawArgs || rawArgs.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
const resolved = [];
|
|
44
|
+
for (const arg of rawArgs) {
|
|
45
|
+
if (typeof arg === 'string' && arg.startsWith('?.')) {
|
|
46
|
+
resolved.push(getValue(arg, this.permissions));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
resolved.push(arg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
33
54
|
redirectRestrictedProp(target, key, value) {
|
|
34
55
|
if (!this.props.has(key))
|
|
35
56
|
return false;
|
|
@@ -151,16 +172,22 @@ function normalizeAttrNames(attr, propNames) {
|
|
|
151
172
|
return propNames;
|
|
152
173
|
return normalizeStrings(attr);
|
|
153
174
|
}
|
|
154
|
-
function
|
|
175
|
+
function buildMethodMaps(permissions) {
|
|
155
176
|
const methodSettings = permissions?.restrictedMethodSettings;
|
|
156
|
-
const
|
|
177
|
+
const blockedMethods = new Set();
|
|
178
|
+
const configuredMethods = new Map();
|
|
157
179
|
if (!methodSettings || methodSettings.length === 0) {
|
|
158
|
-
return
|
|
180
|
+
return { blockedMethods, configuredMethods };
|
|
159
181
|
}
|
|
160
182
|
for (const setting of methodSettings) {
|
|
161
183
|
if (typeof setting === 'string') {
|
|
162
|
-
|
|
184
|
+
blockedMethods.add(setting);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (configuredMethods.has(setting.method)) {
|
|
188
|
+
throw new Error(`assignGingerly: duplicate restrictedMethodSettings entry for '${setting.method}'.`);
|
|
163
189
|
}
|
|
190
|
+
configuredMethods.set(setting.method, setting);
|
|
164
191
|
}
|
|
165
|
-
return
|
|
192
|
+
return { blockedMethods, configuredMethods };
|
|
166
193
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AssignPermissions, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
|
|
1
|
+
import type { AssignPermissions, RestrictedMethodConfig, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
|
|
2
|
+
import { getValue } from '../resolve/getValues.js';
|
|
2
3
|
import { isAllowedUrl } from './isAllowedUrl.js';
|
|
3
4
|
|
|
4
5
|
export interface RestrictedPropSettingsMap {
|
|
@@ -10,7 +11,8 @@ export class PermissionProcessor {
|
|
|
10
11
|
private readonly permissions: AssignPermissions | undefined;
|
|
11
12
|
private readonly props: Map<string, RestrictedPropSetting | undefined>;
|
|
12
13
|
private readonly attrs: Map<string, RestrictedPropSetting | undefined>;
|
|
13
|
-
private readonly
|
|
14
|
+
private readonly blockedMethods: Set<string>;
|
|
15
|
+
private readonly configuredMethods: Map<string, RestrictedMethodConfig>;
|
|
14
16
|
private readonly warned = new Set<string>();
|
|
15
17
|
private readonly warnedMethods = new Set<string>();
|
|
16
18
|
|
|
@@ -19,7 +21,9 @@ export class PermissionProcessor {
|
|
|
19
21
|
const { props, attrs } = buildMaps(permissions);
|
|
20
22
|
this.props = props;
|
|
21
23
|
this.attrs = attrs;
|
|
22
|
-
|
|
24
|
+
const { blockedMethods, configuredMethods } = buildMethodMaps(permissions);
|
|
25
|
+
this.blockedMethods = blockedMethods;
|
|
26
|
+
this.configuredMethods = configuredMethods;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
get crossDomainImports(): boolean {
|
|
@@ -41,11 +45,36 @@ export class PermissionProcessor {
|
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
checkRestrictedMethod(methodName: string): boolean {
|
|
44
|
-
if (!this.
|
|
48
|
+
if (!this.blockedMethods.has(methodName)) return false;
|
|
45
49
|
this.warnRestrictedMethod(methodName);
|
|
46
50
|
return true;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Returns the resolved appendArgs for a configured method, if any.
|
|
55
|
+
* - String entries that start with `?.` are resolved against the permissions object.
|
|
56
|
+
* - Non-path strings are returned as-is.
|
|
57
|
+
* - Methods listed as plain strings in restrictedMethodSettings do not return args;
|
|
58
|
+
* they are fully blocked via checkRestrictedMethod.
|
|
59
|
+
*/
|
|
60
|
+
getMethodAppendArgs(methodName: string): any[] | undefined {
|
|
61
|
+
const config = this.configuredMethods.get(methodName);
|
|
62
|
+
if (!config) return undefined;
|
|
63
|
+
|
|
64
|
+
const rawArgs = config.appendArgs ?? config.addArgs;
|
|
65
|
+
if (!rawArgs || rawArgs.length === 0) return undefined;
|
|
66
|
+
|
|
67
|
+
const resolved: any[] = [];
|
|
68
|
+
for (const arg of rawArgs) {
|
|
69
|
+
if (typeof arg === 'string' && arg.startsWith('?.')) {
|
|
70
|
+
resolved.push(getValue(arg, this.permissions));
|
|
71
|
+
} else {
|
|
72
|
+
resolved.push(arg);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
redirectRestrictedProp(target: any, key: string, value: any): boolean {
|
|
50
79
|
if (!this.props.has(key)) return false;
|
|
51
80
|
const setting = this.props.get(key);
|
|
@@ -192,19 +221,25 @@ function normalizeAttrNames(
|
|
|
192
221
|
return normalizeStrings(attr);
|
|
193
222
|
}
|
|
194
223
|
|
|
195
|
-
function
|
|
224
|
+
function buildMethodMaps(permissions: AssignPermissions | undefined): { blockedMethods: Set<string>; configuredMethods: Map<string, RestrictedMethodConfig> } {
|
|
196
225
|
const methodSettings = permissions?.restrictedMethodSettings;
|
|
197
|
-
const
|
|
226
|
+
const blockedMethods = new Set<string>();
|
|
227
|
+
const configuredMethods = new Map<string, RestrictedMethodConfig>();
|
|
198
228
|
if (!methodSettings || methodSettings.length === 0) {
|
|
199
|
-
return
|
|
229
|
+
return { blockedMethods, configuredMethods };
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
for (const setting of methodSettings) {
|
|
203
233
|
if (typeof setting === 'string') {
|
|
204
|
-
|
|
234
|
+
blockedMethods.add(setting);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (configuredMethods.has(setting.method)) {
|
|
239
|
+
throw new Error(`assignGingerly: duplicate restrictedMethodSettings entry for '${setting.method}'.`);
|
|
205
240
|
}
|
|
206
|
-
|
|
241
|
+
configuredMethods.set(setting.method, setting);
|
|
207
242
|
}
|
|
208
243
|
|
|
209
|
-
return
|
|
244
|
+
return { blockedMethods, configuredMethods };
|
|
210
245
|
}
|
|
@@ -43,13 +43,14 @@ export async function evaluatePathWithAsyncMethods(target, pathParts, value, wit
|
|
|
43
43
|
// Async method — call and await
|
|
44
44
|
const method = current[baseName];
|
|
45
45
|
if (typeof method === 'function') {
|
|
46
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
|
|
46
47
|
if (isZeroArgAsync || nextIsMethod) {
|
|
47
48
|
// Zero-arg call — next is either a method or explicitly not an argument
|
|
48
|
-
current = await method.call(current);
|
|
49
|
+
current = await method.call(current, ...appendArgs);
|
|
49
50
|
}
|
|
50
51
|
else {
|
|
51
52
|
// Call with next part as string arg, then await
|
|
52
|
-
current = await method.call(current, nextPart);
|
|
53
|
+
current = await method.call(current, nextPart, ...appendArgs);
|
|
53
54
|
i++; // Skip next part since we consumed it as argument
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -65,13 +66,14 @@ export async function evaluatePathWithAsyncMethods(target, pathParts, value, wit
|
|
|
65
66
|
// Sync method — same logic as evaluatePathWithMethods
|
|
66
67
|
const method = current[baseName];
|
|
67
68
|
if (typeof method === 'function') {
|
|
69
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
|
|
68
70
|
if (isZeroArgSync || nextIsMethod) {
|
|
69
71
|
// Zero-arg call — next is either a method or explicitly not an argument
|
|
70
|
-
current = method.call(current);
|
|
72
|
+
current = method.call(current, ...appendArgs);
|
|
71
73
|
}
|
|
72
74
|
else {
|
|
73
75
|
// Call with next part as string arg
|
|
74
|
-
current = method.call(current, nextPart);
|
|
76
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
75
77
|
i++; // Skip next part since we consumed it as argument
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -69,12 +69,13 @@ export async function evaluatePathWithAsyncMethods(
|
|
|
69
69
|
// Async method — call and await
|
|
70
70
|
const method = current[baseName];
|
|
71
71
|
if (typeof method === 'function') {
|
|
72
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
|
|
72
73
|
if (isZeroArgAsync || nextIsMethod) {
|
|
73
74
|
// Zero-arg call — next is either a method or explicitly not an argument
|
|
74
|
-
current = await method.call(current);
|
|
75
|
+
current = await method.call(current, ...appendArgs);
|
|
75
76
|
} else {
|
|
76
77
|
// Call with next part as string arg, then await
|
|
77
|
-
current = await method.call(current, nextPart);
|
|
78
|
+
current = await method.call(current, nextPart, ...appendArgs);
|
|
78
79
|
i++; // Skip next part since we consumed it as argument
|
|
79
80
|
}
|
|
80
81
|
} else {
|
|
@@ -88,12 +89,13 @@ export async function evaluatePathWithAsyncMethods(
|
|
|
88
89
|
// Sync method — same logic as evaluatePathWithMethods
|
|
89
90
|
const method = current[baseName];
|
|
90
91
|
if (typeof method === 'function') {
|
|
92
|
+
const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
|
|
91
93
|
if (isZeroArgSync || nextIsMethod) {
|
|
92
94
|
// Zero-arg call — next is either a method or explicitly not an argument
|
|
93
|
-
current = method.call(current);
|
|
95
|
+
current = method.call(current, ...appendArgs);
|
|
94
96
|
} else {
|
|
95
97
|
// Call with next part as string arg
|
|
96
|
-
current = method.call(current, nextPart);
|
|
98
|
+
current = method.call(current, nextPart, ...appendArgs);
|
|
97
99
|
i++; // Skip next part since we consumed it as argument
|
|
98
100
|
}
|
|
99
101
|
} else {
|
package/handleIshProperty.js
CHANGED
|
@@ -47,7 +47,7 @@ async function defineIshProperty(element, managerName, options, assignGingerlyFn
|
|
|
47
47
|
let config = registry.get(managerName);
|
|
48
48
|
// If not registered, wait for registration
|
|
49
49
|
if (!config) {
|
|
50
|
-
const { waitForEvent } = await import('./waitForEvent.js');
|
|
50
|
+
const { waitForEvent } = await import('./utils/waitForEvent.js');
|
|
51
51
|
await waitForEvent(registry, managerName);
|
|
52
52
|
config = registry.get(managerName);
|
|
53
53
|
if (!config) {
|
package/handleIshProperty.ts
CHANGED
|
@@ -66,7 +66,7 @@ async function defineIshProperty(
|
|
|
66
66
|
|
|
67
67
|
// If not registered, wait for registration
|
|
68
68
|
if (!config) {
|
|
69
|
-
const { waitForEvent } = await import('./waitForEvent.js');
|
|
69
|
+
const { waitForEvent } = await import('./utils/waitForEvent.js');
|
|
70
70
|
await waitForEvent(registry, managerName);
|
|
71
71
|
config = registry.get(managerName);
|
|
72
72
|
|