backtest-kit 2.0.3 → 2.0.5
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/build/index.cjs +245 -16
- package/build/index.mjs +245 -16
- package/package.json +1 -1
- package/types.d.ts +50 -11
package/build/index.cjs
CHANGED
|
@@ -783,10 +783,14 @@ class ClientExchange {
|
|
|
783
783
|
// Apply distinct by timestamp to remove duplicates
|
|
784
784
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
785
785
|
if (filteredData.length !== uniqueData.length) {
|
|
786
|
-
|
|
786
|
+
const msg = `ClientExchange Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
787
|
+
this.params.logger.warn(msg);
|
|
788
|
+
console.warn(msg);
|
|
787
789
|
}
|
|
788
790
|
if (uniqueData.length < limit) {
|
|
789
|
-
|
|
791
|
+
const msg = `ClientExchange Expected ${limit} candles, got ${uniqueData.length}`;
|
|
792
|
+
this.params.logger.warn(msg);
|
|
793
|
+
console.warn(msg);
|
|
790
794
|
}
|
|
791
795
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
792
796
|
return uniqueData;
|
|
@@ -9896,11 +9900,218 @@ class RiskSchemaService {
|
|
|
9896
9900
|
}
|
|
9897
9901
|
}
|
|
9898
9902
|
|
|
9903
|
+
/**
|
|
9904
|
+
* List of valid method names allowed in action handlers.
|
|
9905
|
+
* Any public methods not in this list will trigger validation errors.
|
|
9906
|
+
* Private methods (starting with _ or #) are ignored during validation.
|
|
9907
|
+
*/
|
|
9908
|
+
const VALID_METHOD_NAMES = [
|
|
9909
|
+
"init",
|
|
9910
|
+
"signal",
|
|
9911
|
+
"signalLive",
|
|
9912
|
+
"signalBacktest",
|
|
9913
|
+
"breakevenAvailable",
|
|
9914
|
+
"partialProfitAvailable",
|
|
9915
|
+
"partialLossAvailable",
|
|
9916
|
+
"pingScheduled",
|
|
9917
|
+
"pingActive",
|
|
9918
|
+
"riskRejection",
|
|
9919
|
+
"dispose",
|
|
9920
|
+
];
|
|
9921
|
+
/**
|
|
9922
|
+
* Calculates the Levenshtein distance between two strings.
|
|
9923
|
+
*
|
|
9924
|
+
* Levenshtein distance is the minimum number of single-character edits
|
|
9925
|
+
* (insertions, deletions, or substitutions) required to change one string into another.
|
|
9926
|
+
* Used to find typos and similar method names in validation.
|
|
9927
|
+
*
|
|
9928
|
+
* @param str1 - First string to compare
|
|
9929
|
+
* @param str2 - Second string to compare
|
|
9930
|
+
* @returns Number of edits needed to transform str1 into str2
|
|
9931
|
+
*/
|
|
9932
|
+
const LEVENSHTEIN_DISTANCE = (str1, str2) => {
|
|
9933
|
+
const len1 = str1.length;
|
|
9934
|
+
const len2 = str2.length;
|
|
9935
|
+
// Create a 2D array for dynamic programming
|
|
9936
|
+
const matrix = Array.from({ length: len1 + 1 }, () => Array(len2 + 1).fill(0));
|
|
9937
|
+
// Initialize first column and row
|
|
9938
|
+
for (let i = 0; i <= len1; i++) {
|
|
9939
|
+
matrix[i][0] = i;
|
|
9940
|
+
}
|
|
9941
|
+
for (let j = 0; j <= len2; j++) {
|
|
9942
|
+
matrix[0][j] = j;
|
|
9943
|
+
}
|
|
9944
|
+
// Fill the matrix
|
|
9945
|
+
for (let i = 1; i <= len1; i++) {
|
|
9946
|
+
for (let j = 1; j <= len2; j++) {
|
|
9947
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
9948
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
|
|
9949
|
+
matrix[i][j - 1] + 1, // insertion
|
|
9950
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
9951
|
+
);
|
|
9952
|
+
}
|
|
9953
|
+
}
|
|
9954
|
+
return matrix[len1][len2];
|
|
9955
|
+
};
|
|
9956
|
+
/**
|
|
9957
|
+
* Finds suggestions for a method name based on similarity scoring.
|
|
9958
|
+
*
|
|
9959
|
+
* Uses Levenshtein distance and partial string matching to find similar method names.
|
|
9960
|
+
* Returns suggestions sorted by similarity (most similar first).
|
|
9961
|
+
* Used to provide helpful "Did you mean?" suggestions in validation error messages.
|
|
9962
|
+
*
|
|
9963
|
+
* @param methodName - The invalid method name to find suggestions for
|
|
9964
|
+
* @param validNames - List of valid method names to search through
|
|
9965
|
+
* @param maxDistance - Maximum Levenshtein distance to consider (default: 3)
|
|
9966
|
+
* @returns Array of suggested method names sorted by similarity
|
|
9967
|
+
*/
|
|
9968
|
+
const FIND_SUGGESTIONS = (methodName, validNames, maxDistance = 3) => {
|
|
9969
|
+
const lowerMethodName = methodName.toLowerCase();
|
|
9970
|
+
// Calculate similarity score for each valid name
|
|
9971
|
+
const suggestions = validNames
|
|
9972
|
+
.map((validName) => {
|
|
9973
|
+
const lowerValidName = validName.toLowerCase();
|
|
9974
|
+
const distance = LEVENSHTEIN_DISTANCE(lowerMethodName, lowerValidName);
|
|
9975
|
+
// Check for partial matches
|
|
9976
|
+
const hasPartialMatch = lowerValidName.includes(lowerMethodName) ||
|
|
9977
|
+
lowerMethodName.includes(lowerValidName);
|
|
9978
|
+
return {
|
|
9979
|
+
name: validName,
|
|
9980
|
+
distance,
|
|
9981
|
+
hasPartialMatch,
|
|
9982
|
+
};
|
|
9983
|
+
})
|
|
9984
|
+
.filter((item) => item.distance <= maxDistance || item.hasPartialMatch)
|
|
9985
|
+
.sort((a, b) => {
|
|
9986
|
+
// Prioritize partial matches
|
|
9987
|
+
if (a.hasPartialMatch && !b.hasPartialMatch)
|
|
9988
|
+
return -1;
|
|
9989
|
+
if (!a.hasPartialMatch && b.hasPartialMatch)
|
|
9990
|
+
return 1;
|
|
9991
|
+
// Then sort by distance
|
|
9992
|
+
return a.distance - b.distance;
|
|
9993
|
+
})
|
|
9994
|
+
.slice(0, 3) // Limit to top 3 suggestions
|
|
9995
|
+
.map((item) => item.name);
|
|
9996
|
+
return suggestions;
|
|
9997
|
+
};
|
|
9998
|
+
/**
|
|
9999
|
+
* Validates that all public methods in a class-based action handler are in the allowed list.
|
|
10000
|
+
*
|
|
10001
|
+
* Inspects the class prototype to find all method names and ensures they match
|
|
10002
|
+
* the VALID_METHOD_NAMES list. Private methods (starting with _ or #) are skipped.
|
|
10003
|
+
* Private fields with # are not visible via Object.getOwnPropertyNames() and don't
|
|
10004
|
+
* need validation as they're truly private and inaccessible.
|
|
10005
|
+
*
|
|
10006
|
+
* @param actionName - Name of the action being validated
|
|
10007
|
+
* @param handler - Class constructor for the action handler
|
|
10008
|
+
* @param self - ActionSchemaService instance for logging
|
|
10009
|
+
* @throws Error if any public method is not in VALID_METHOD_NAMES
|
|
10010
|
+
*/
|
|
10011
|
+
const VALIDATE_CLASS_METHODS = (actionName, handler, self) => {
|
|
10012
|
+
// Get all method names from prototype (for classes)
|
|
10013
|
+
// Note: Private fields with # are not visible via Object.getOwnPropertyNames()
|
|
10014
|
+
// and don't need validation as they're truly private and inaccessible
|
|
10015
|
+
const prototypeProps = Object.getOwnPropertyNames(handler.prototype);
|
|
10016
|
+
for (const methodName of prototypeProps) {
|
|
10017
|
+
// Skip constructor and conventionally private methods (starting with _)
|
|
10018
|
+
if (methodName === "constructor" || methodName.startsWith("_")) {
|
|
10019
|
+
continue;
|
|
10020
|
+
}
|
|
10021
|
+
const descriptor = Object.getOwnPropertyDescriptor(handler.prototype, methodName);
|
|
10022
|
+
const isMethod = descriptor && typeof descriptor.value === "function";
|
|
10023
|
+
if (isMethod && !VALID_METHOD_NAMES.includes(methodName)) {
|
|
10024
|
+
const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
|
|
10025
|
+
const lines = [
|
|
10026
|
+
`ActionSchema ${actionName} contains invalid method "${methodName}". `,
|
|
10027
|
+
`Valid methods are: ${VALID_METHOD_NAMES.join(", ")}`,
|
|
10028
|
+
];
|
|
10029
|
+
if (suggestions.length > 0) {
|
|
10030
|
+
lines.push("");
|
|
10031
|
+
lines.push(`Do you mean: ${suggestions.join(", ")}?`);
|
|
10032
|
+
lines.push("");
|
|
10033
|
+
}
|
|
10034
|
+
lines.push(`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`);
|
|
10035
|
+
const msg = functoolsKit.str.newline(lines);
|
|
10036
|
+
self.loggerService.log(`actionValidationService exception thrown`, {
|
|
10037
|
+
msg,
|
|
10038
|
+
});
|
|
10039
|
+
throw new Error(msg);
|
|
10040
|
+
}
|
|
10041
|
+
}
|
|
10042
|
+
};
|
|
10043
|
+
/**
|
|
10044
|
+
* Validates that all public methods in an object-based action handler are in the allowed list.
|
|
10045
|
+
*
|
|
10046
|
+
* Inspects the object's own properties to find all method names and ensures they match
|
|
10047
|
+
* the VALID_METHOD_NAMES list. Private properties (starting with _) are skipped.
|
|
10048
|
+
*
|
|
10049
|
+
* @param actionName - Name of the action being validated
|
|
10050
|
+
* @param handler - Plain object implementing partial IPublicAction interface
|
|
10051
|
+
* @param self - ActionSchemaService instance for logging
|
|
10052
|
+
* @throws Error if any public method is not in VALID_METHOD_NAMES
|
|
10053
|
+
*/
|
|
10054
|
+
const VALIDATE_OBJECT_METHODS = (actionName, handler, self) => {
|
|
10055
|
+
// For plain objects (Partial<IPublicAction>)
|
|
10056
|
+
const methodNames = Object.keys(handler);
|
|
10057
|
+
for (const methodName of methodNames) {
|
|
10058
|
+
// Skip private properties (starting with _)
|
|
10059
|
+
if (methodName.startsWith("_")) {
|
|
10060
|
+
continue;
|
|
10061
|
+
}
|
|
10062
|
+
if (typeof handler[methodName] === "function" &&
|
|
10063
|
+
!VALID_METHOD_NAMES.includes(methodName)) {
|
|
10064
|
+
const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
|
|
10065
|
+
const lines = [
|
|
10066
|
+
`ActionSchema ${actionName} contains invalid method "${methodName}". `,
|
|
10067
|
+
`Valid methods are: ${VALID_METHOD_NAMES.join(", ")}`,
|
|
10068
|
+
];
|
|
10069
|
+
if (suggestions.length > 0) {
|
|
10070
|
+
lines.push("");
|
|
10071
|
+
lines.push(`Do you mean: ${suggestions.join(", ")}?`);
|
|
10072
|
+
lines.push("");
|
|
10073
|
+
}
|
|
10074
|
+
lines.push(`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`);
|
|
10075
|
+
const msg = functoolsKit.str.newline(lines);
|
|
10076
|
+
self.loggerService.log(`actionValidationService exception thrown`, {
|
|
10077
|
+
msg,
|
|
10078
|
+
});
|
|
10079
|
+
throw new Error(msg);
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
};
|
|
9899
10083
|
/**
|
|
9900
10084
|
* Service for managing action schema registry.
|
|
9901
10085
|
*
|
|
10086
|
+
* Manages registration, validation and retrieval of action schemas.
|
|
9902
10087
|
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
9903
|
-
*
|
|
10088
|
+
* Validates that action handlers only contain allowed public methods
|
|
10089
|
+
* from the IPublicAction interface.
|
|
10090
|
+
*
|
|
10091
|
+
* Key features:
|
|
10092
|
+
* - Type-safe action schema registration
|
|
10093
|
+
* - Method name validation for class and object handlers
|
|
10094
|
+
* - Private method support (methods starting with _ or #)
|
|
10095
|
+
* - Schema override capabilities
|
|
10096
|
+
*
|
|
10097
|
+
* @example
|
|
10098
|
+
* ```typescript
|
|
10099
|
+
* // Register a class-based action
|
|
10100
|
+
* actionSchemaService.register("telegram-notifier", {
|
|
10101
|
+
* actionName: "telegram-notifier",
|
|
10102
|
+
* handler: TelegramNotifierAction,
|
|
10103
|
+
* callbacks: { ... }
|
|
10104
|
+
* });
|
|
10105
|
+
*
|
|
10106
|
+
* // Register an object-based action
|
|
10107
|
+
* actionSchemaService.register("logger", {
|
|
10108
|
+
* actionName: "logger",
|
|
10109
|
+
* handler: {
|
|
10110
|
+
* signal: async (event) => { ... },
|
|
10111
|
+
* dispose: async () => { ... }
|
|
10112
|
+
* }
|
|
10113
|
+
* });
|
|
10114
|
+
* ```
|
|
9904
10115
|
*/
|
|
9905
10116
|
class ActionSchemaService {
|
|
9906
10117
|
constructor() {
|
|
@@ -9909,9 +10120,13 @@ class ActionSchemaService {
|
|
|
9909
10120
|
/**
|
|
9910
10121
|
* Registers a new action schema.
|
|
9911
10122
|
*
|
|
9912
|
-
*
|
|
9913
|
-
*
|
|
9914
|
-
*
|
|
10123
|
+
* Validates the schema structure and method names before registration.
|
|
10124
|
+
* Throws an error if the action name already exists in the registry.
|
|
10125
|
+
*
|
|
10126
|
+
* @param key - Unique action name identifier
|
|
10127
|
+
* @param value - Action schema configuration with handler and optional callbacks
|
|
10128
|
+
* @throws Error if action name already exists in registry
|
|
10129
|
+
* @throws Error if validation fails (missing required fields, invalid handler, invalid method names)
|
|
9915
10130
|
*/
|
|
9916
10131
|
this.register = (key, value) => {
|
|
9917
10132
|
this.loggerService.log(`actionSchemaService register`, { key });
|
|
@@ -9923,11 +10138,13 @@ class ActionSchemaService {
|
|
|
9923
10138
|
*
|
|
9924
10139
|
* Performs shallow validation to ensure all required properties exist
|
|
9925
10140
|
* and have correct types before registration in the registry.
|
|
10141
|
+
* Also validates that all public methods in the handler are allowed.
|
|
9926
10142
|
*
|
|
9927
10143
|
* @param actionSchema - Action schema to validate
|
|
9928
10144
|
* @throws Error if actionName is missing or not a string
|
|
9929
|
-
* @throws Error if handler is
|
|
9930
|
-
* @throws Error if
|
|
10145
|
+
* @throws Error if handler is not a function or plain object
|
|
10146
|
+
* @throws Error if handler contains invalid public method names
|
|
10147
|
+
* @throws Error if callbacks is provided but not an object
|
|
9931
10148
|
*/
|
|
9932
10149
|
this.validateShallow = (actionSchema) => {
|
|
9933
10150
|
this.loggerService.log(`actionSchemaService validateShallow`, {
|
|
@@ -9936,21 +10153,30 @@ class ActionSchemaService {
|
|
|
9936
10153
|
if (typeof actionSchema.actionName !== "string") {
|
|
9937
10154
|
throw new Error(`action schema validation failed: missing actionName`);
|
|
9938
10155
|
}
|
|
9939
|
-
if (typeof actionSchema.handler !== "function" &&
|
|
10156
|
+
if (typeof actionSchema.handler !== "function" &&
|
|
10157
|
+
!functoolsKit.isObject(actionSchema.handler)) {
|
|
9940
10158
|
throw new Error(`action schema validation failed: handler is not a function or plain object for actionName=${actionSchema.actionName}`);
|
|
9941
10159
|
}
|
|
9942
|
-
if (actionSchema.
|
|
9943
|
-
|
|
10160
|
+
if (typeof actionSchema.handler === "function" && actionSchema.handler.prototype) {
|
|
10161
|
+
VALIDATE_CLASS_METHODS(actionSchema.actionName, actionSchema.handler, this);
|
|
10162
|
+
}
|
|
10163
|
+
if (typeof actionSchema.handler === "object" && actionSchema.handler !== null) {
|
|
10164
|
+
VALIDATE_OBJECT_METHODS(actionSchema.actionName, actionSchema.handler, this);
|
|
10165
|
+
}
|
|
10166
|
+
if (actionSchema.callbacks && !functoolsKit.isObject(actionSchema.callbacks)) {
|
|
9944
10167
|
throw new Error(`action schema validation failed: callbacks is not an object for actionName=${actionSchema.actionName}`);
|
|
9945
10168
|
}
|
|
9946
10169
|
};
|
|
9947
10170
|
/**
|
|
9948
10171
|
* Overrides an existing action schema with partial updates.
|
|
9949
10172
|
*
|
|
10173
|
+
* Merges provided partial schema updates with the existing schema.
|
|
10174
|
+
* Useful for modifying handler or callbacks without re-registering the entire schema.
|
|
10175
|
+
*
|
|
9950
10176
|
* @param key - Action name to override
|
|
9951
|
-
* @param value - Partial schema updates
|
|
9952
|
-
* @returns Updated action schema
|
|
9953
|
-
* @throws Error if action name doesn't exist
|
|
10177
|
+
* @param value - Partial schema updates to merge
|
|
10178
|
+
* @returns Updated action schema after override
|
|
10179
|
+
* @throws Error if action name doesn't exist in registry
|
|
9954
10180
|
*/
|
|
9955
10181
|
this.override = (key, value) => {
|
|
9956
10182
|
this.loggerService.log(`actionSchemaService override`, { key });
|
|
@@ -9960,9 +10186,12 @@ class ActionSchemaService {
|
|
|
9960
10186
|
/**
|
|
9961
10187
|
* Retrieves an action schema by name.
|
|
9962
10188
|
*
|
|
9963
|
-
*
|
|
10189
|
+
* Returns the complete action schema configuration including handler and callbacks.
|
|
10190
|
+
* Used internally by ActionConnectionService to instantiate ClientAction instances.
|
|
10191
|
+
*
|
|
10192
|
+
* @param key - Action name identifier
|
|
9964
10193
|
* @returns Action schema configuration
|
|
9965
|
-
* @throws Error if action name doesn't exist
|
|
10194
|
+
* @throws Error if action name doesn't exist in registry
|
|
9966
10195
|
*/
|
|
9967
10196
|
this.get = (key) => {
|
|
9968
10197
|
this.loggerService.log(`actionSchemaService get`, { key });
|
package/build/index.mjs
CHANGED
|
@@ -763,10 +763,14 @@ class ClientExchange {
|
|
|
763
763
|
// Apply distinct by timestamp to remove duplicates
|
|
764
764
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
765
765
|
if (filteredData.length !== uniqueData.length) {
|
|
766
|
-
|
|
766
|
+
const msg = `ClientExchange Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
767
|
+
this.params.logger.warn(msg);
|
|
768
|
+
console.warn(msg);
|
|
767
769
|
}
|
|
768
770
|
if (uniqueData.length < limit) {
|
|
769
|
-
|
|
771
|
+
const msg = `ClientExchange Expected ${limit} candles, got ${uniqueData.length}`;
|
|
772
|
+
this.params.logger.warn(msg);
|
|
773
|
+
console.warn(msg);
|
|
770
774
|
}
|
|
771
775
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
772
776
|
return uniqueData;
|
|
@@ -9876,11 +9880,218 @@ class RiskSchemaService {
|
|
|
9876
9880
|
}
|
|
9877
9881
|
}
|
|
9878
9882
|
|
|
9883
|
+
/**
|
|
9884
|
+
* List of valid method names allowed in action handlers.
|
|
9885
|
+
* Any public methods not in this list will trigger validation errors.
|
|
9886
|
+
* Private methods (starting with _ or #) are ignored during validation.
|
|
9887
|
+
*/
|
|
9888
|
+
const VALID_METHOD_NAMES = [
|
|
9889
|
+
"init",
|
|
9890
|
+
"signal",
|
|
9891
|
+
"signalLive",
|
|
9892
|
+
"signalBacktest",
|
|
9893
|
+
"breakevenAvailable",
|
|
9894
|
+
"partialProfitAvailable",
|
|
9895
|
+
"partialLossAvailable",
|
|
9896
|
+
"pingScheduled",
|
|
9897
|
+
"pingActive",
|
|
9898
|
+
"riskRejection",
|
|
9899
|
+
"dispose",
|
|
9900
|
+
];
|
|
9901
|
+
/**
|
|
9902
|
+
* Calculates the Levenshtein distance between two strings.
|
|
9903
|
+
*
|
|
9904
|
+
* Levenshtein distance is the minimum number of single-character edits
|
|
9905
|
+
* (insertions, deletions, or substitutions) required to change one string into another.
|
|
9906
|
+
* Used to find typos and similar method names in validation.
|
|
9907
|
+
*
|
|
9908
|
+
* @param str1 - First string to compare
|
|
9909
|
+
* @param str2 - Second string to compare
|
|
9910
|
+
* @returns Number of edits needed to transform str1 into str2
|
|
9911
|
+
*/
|
|
9912
|
+
const LEVENSHTEIN_DISTANCE = (str1, str2) => {
|
|
9913
|
+
const len1 = str1.length;
|
|
9914
|
+
const len2 = str2.length;
|
|
9915
|
+
// Create a 2D array for dynamic programming
|
|
9916
|
+
const matrix = Array.from({ length: len1 + 1 }, () => Array(len2 + 1).fill(0));
|
|
9917
|
+
// Initialize first column and row
|
|
9918
|
+
for (let i = 0; i <= len1; i++) {
|
|
9919
|
+
matrix[i][0] = i;
|
|
9920
|
+
}
|
|
9921
|
+
for (let j = 0; j <= len2; j++) {
|
|
9922
|
+
matrix[0][j] = j;
|
|
9923
|
+
}
|
|
9924
|
+
// Fill the matrix
|
|
9925
|
+
for (let i = 1; i <= len1; i++) {
|
|
9926
|
+
for (let j = 1; j <= len2; j++) {
|
|
9927
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
9928
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
|
|
9929
|
+
matrix[i][j - 1] + 1, // insertion
|
|
9930
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
9931
|
+
);
|
|
9932
|
+
}
|
|
9933
|
+
}
|
|
9934
|
+
return matrix[len1][len2];
|
|
9935
|
+
};
|
|
9936
|
+
/**
|
|
9937
|
+
* Finds suggestions for a method name based on similarity scoring.
|
|
9938
|
+
*
|
|
9939
|
+
* Uses Levenshtein distance and partial string matching to find similar method names.
|
|
9940
|
+
* Returns suggestions sorted by similarity (most similar first).
|
|
9941
|
+
* Used to provide helpful "Did you mean?" suggestions in validation error messages.
|
|
9942
|
+
*
|
|
9943
|
+
* @param methodName - The invalid method name to find suggestions for
|
|
9944
|
+
* @param validNames - List of valid method names to search through
|
|
9945
|
+
* @param maxDistance - Maximum Levenshtein distance to consider (default: 3)
|
|
9946
|
+
* @returns Array of suggested method names sorted by similarity
|
|
9947
|
+
*/
|
|
9948
|
+
const FIND_SUGGESTIONS = (methodName, validNames, maxDistance = 3) => {
|
|
9949
|
+
const lowerMethodName = methodName.toLowerCase();
|
|
9950
|
+
// Calculate similarity score for each valid name
|
|
9951
|
+
const suggestions = validNames
|
|
9952
|
+
.map((validName) => {
|
|
9953
|
+
const lowerValidName = validName.toLowerCase();
|
|
9954
|
+
const distance = LEVENSHTEIN_DISTANCE(lowerMethodName, lowerValidName);
|
|
9955
|
+
// Check for partial matches
|
|
9956
|
+
const hasPartialMatch = lowerValidName.includes(lowerMethodName) ||
|
|
9957
|
+
lowerMethodName.includes(lowerValidName);
|
|
9958
|
+
return {
|
|
9959
|
+
name: validName,
|
|
9960
|
+
distance,
|
|
9961
|
+
hasPartialMatch,
|
|
9962
|
+
};
|
|
9963
|
+
})
|
|
9964
|
+
.filter((item) => item.distance <= maxDistance || item.hasPartialMatch)
|
|
9965
|
+
.sort((a, b) => {
|
|
9966
|
+
// Prioritize partial matches
|
|
9967
|
+
if (a.hasPartialMatch && !b.hasPartialMatch)
|
|
9968
|
+
return -1;
|
|
9969
|
+
if (!a.hasPartialMatch && b.hasPartialMatch)
|
|
9970
|
+
return 1;
|
|
9971
|
+
// Then sort by distance
|
|
9972
|
+
return a.distance - b.distance;
|
|
9973
|
+
})
|
|
9974
|
+
.slice(0, 3) // Limit to top 3 suggestions
|
|
9975
|
+
.map((item) => item.name);
|
|
9976
|
+
return suggestions;
|
|
9977
|
+
};
|
|
9978
|
+
/**
|
|
9979
|
+
* Validates that all public methods in a class-based action handler are in the allowed list.
|
|
9980
|
+
*
|
|
9981
|
+
* Inspects the class prototype to find all method names and ensures they match
|
|
9982
|
+
* the VALID_METHOD_NAMES list. Private methods (starting with _ or #) are skipped.
|
|
9983
|
+
* Private fields with # are not visible via Object.getOwnPropertyNames() and don't
|
|
9984
|
+
* need validation as they're truly private and inaccessible.
|
|
9985
|
+
*
|
|
9986
|
+
* @param actionName - Name of the action being validated
|
|
9987
|
+
* @param handler - Class constructor for the action handler
|
|
9988
|
+
* @param self - ActionSchemaService instance for logging
|
|
9989
|
+
* @throws Error if any public method is not in VALID_METHOD_NAMES
|
|
9990
|
+
*/
|
|
9991
|
+
const VALIDATE_CLASS_METHODS = (actionName, handler, self) => {
|
|
9992
|
+
// Get all method names from prototype (for classes)
|
|
9993
|
+
// Note: Private fields with # are not visible via Object.getOwnPropertyNames()
|
|
9994
|
+
// and don't need validation as they're truly private and inaccessible
|
|
9995
|
+
const prototypeProps = Object.getOwnPropertyNames(handler.prototype);
|
|
9996
|
+
for (const methodName of prototypeProps) {
|
|
9997
|
+
// Skip constructor and conventionally private methods (starting with _)
|
|
9998
|
+
if (methodName === "constructor" || methodName.startsWith("_")) {
|
|
9999
|
+
continue;
|
|
10000
|
+
}
|
|
10001
|
+
const descriptor = Object.getOwnPropertyDescriptor(handler.prototype, methodName);
|
|
10002
|
+
const isMethod = descriptor && typeof descriptor.value === "function";
|
|
10003
|
+
if (isMethod && !VALID_METHOD_NAMES.includes(methodName)) {
|
|
10004
|
+
const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
|
|
10005
|
+
const lines = [
|
|
10006
|
+
`ActionSchema ${actionName} contains invalid method "${methodName}". `,
|
|
10007
|
+
`Valid methods are: ${VALID_METHOD_NAMES.join(", ")}`,
|
|
10008
|
+
];
|
|
10009
|
+
if (suggestions.length > 0) {
|
|
10010
|
+
lines.push("");
|
|
10011
|
+
lines.push(`Do you mean: ${suggestions.join(", ")}?`);
|
|
10012
|
+
lines.push("");
|
|
10013
|
+
}
|
|
10014
|
+
lines.push(`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`);
|
|
10015
|
+
const msg = str.newline(lines);
|
|
10016
|
+
self.loggerService.log(`actionValidationService exception thrown`, {
|
|
10017
|
+
msg,
|
|
10018
|
+
});
|
|
10019
|
+
throw new Error(msg);
|
|
10020
|
+
}
|
|
10021
|
+
}
|
|
10022
|
+
};
|
|
10023
|
+
/**
|
|
10024
|
+
* Validates that all public methods in an object-based action handler are in the allowed list.
|
|
10025
|
+
*
|
|
10026
|
+
* Inspects the object's own properties to find all method names and ensures they match
|
|
10027
|
+
* the VALID_METHOD_NAMES list. Private properties (starting with _) are skipped.
|
|
10028
|
+
*
|
|
10029
|
+
* @param actionName - Name of the action being validated
|
|
10030
|
+
* @param handler - Plain object implementing partial IPublicAction interface
|
|
10031
|
+
* @param self - ActionSchemaService instance for logging
|
|
10032
|
+
* @throws Error if any public method is not in VALID_METHOD_NAMES
|
|
10033
|
+
*/
|
|
10034
|
+
const VALIDATE_OBJECT_METHODS = (actionName, handler, self) => {
|
|
10035
|
+
// For plain objects (Partial<IPublicAction>)
|
|
10036
|
+
const methodNames = Object.keys(handler);
|
|
10037
|
+
for (const methodName of methodNames) {
|
|
10038
|
+
// Skip private properties (starting with _)
|
|
10039
|
+
if (methodName.startsWith("_")) {
|
|
10040
|
+
continue;
|
|
10041
|
+
}
|
|
10042
|
+
if (typeof handler[methodName] === "function" &&
|
|
10043
|
+
!VALID_METHOD_NAMES.includes(methodName)) {
|
|
10044
|
+
const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
|
|
10045
|
+
const lines = [
|
|
10046
|
+
`ActionSchema ${actionName} contains invalid method "${methodName}". `,
|
|
10047
|
+
`Valid methods are: ${VALID_METHOD_NAMES.join(", ")}`,
|
|
10048
|
+
];
|
|
10049
|
+
if (suggestions.length > 0) {
|
|
10050
|
+
lines.push("");
|
|
10051
|
+
lines.push(`Do you mean: ${suggestions.join(", ")}?`);
|
|
10052
|
+
lines.push("");
|
|
10053
|
+
}
|
|
10054
|
+
lines.push(`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`);
|
|
10055
|
+
const msg = str.newline(lines);
|
|
10056
|
+
self.loggerService.log(`actionValidationService exception thrown`, {
|
|
10057
|
+
msg,
|
|
10058
|
+
});
|
|
10059
|
+
throw new Error(msg);
|
|
10060
|
+
}
|
|
10061
|
+
}
|
|
10062
|
+
};
|
|
9879
10063
|
/**
|
|
9880
10064
|
* Service for managing action schema registry.
|
|
9881
10065
|
*
|
|
10066
|
+
* Manages registration, validation and retrieval of action schemas.
|
|
9882
10067
|
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
9883
|
-
*
|
|
10068
|
+
* Validates that action handlers only contain allowed public methods
|
|
10069
|
+
* from the IPublicAction interface.
|
|
10070
|
+
*
|
|
10071
|
+
* Key features:
|
|
10072
|
+
* - Type-safe action schema registration
|
|
10073
|
+
* - Method name validation for class and object handlers
|
|
10074
|
+
* - Private method support (methods starting with _ or #)
|
|
10075
|
+
* - Schema override capabilities
|
|
10076
|
+
*
|
|
10077
|
+
* @example
|
|
10078
|
+
* ```typescript
|
|
10079
|
+
* // Register a class-based action
|
|
10080
|
+
* actionSchemaService.register("telegram-notifier", {
|
|
10081
|
+
* actionName: "telegram-notifier",
|
|
10082
|
+
* handler: TelegramNotifierAction,
|
|
10083
|
+
* callbacks: { ... }
|
|
10084
|
+
* });
|
|
10085
|
+
*
|
|
10086
|
+
* // Register an object-based action
|
|
10087
|
+
* actionSchemaService.register("logger", {
|
|
10088
|
+
* actionName: "logger",
|
|
10089
|
+
* handler: {
|
|
10090
|
+
* signal: async (event) => { ... },
|
|
10091
|
+
* dispose: async () => { ... }
|
|
10092
|
+
* }
|
|
10093
|
+
* });
|
|
10094
|
+
* ```
|
|
9884
10095
|
*/
|
|
9885
10096
|
class ActionSchemaService {
|
|
9886
10097
|
constructor() {
|
|
@@ -9889,9 +10100,13 @@ class ActionSchemaService {
|
|
|
9889
10100
|
/**
|
|
9890
10101
|
* Registers a new action schema.
|
|
9891
10102
|
*
|
|
9892
|
-
*
|
|
9893
|
-
*
|
|
9894
|
-
*
|
|
10103
|
+
* Validates the schema structure and method names before registration.
|
|
10104
|
+
* Throws an error if the action name already exists in the registry.
|
|
10105
|
+
*
|
|
10106
|
+
* @param key - Unique action name identifier
|
|
10107
|
+
* @param value - Action schema configuration with handler and optional callbacks
|
|
10108
|
+
* @throws Error if action name already exists in registry
|
|
10109
|
+
* @throws Error if validation fails (missing required fields, invalid handler, invalid method names)
|
|
9895
10110
|
*/
|
|
9896
10111
|
this.register = (key, value) => {
|
|
9897
10112
|
this.loggerService.log(`actionSchemaService register`, { key });
|
|
@@ -9903,11 +10118,13 @@ class ActionSchemaService {
|
|
|
9903
10118
|
*
|
|
9904
10119
|
* Performs shallow validation to ensure all required properties exist
|
|
9905
10120
|
* and have correct types before registration in the registry.
|
|
10121
|
+
* Also validates that all public methods in the handler are allowed.
|
|
9906
10122
|
*
|
|
9907
10123
|
* @param actionSchema - Action schema to validate
|
|
9908
10124
|
* @throws Error if actionName is missing or not a string
|
|
9909
|
-
* @throws Error if handler is
|
|
9910
|
-
* @throws Error if
|
|
10125
|
+
* @throws Error if handler is not a function or plain object
|
|
10126
|
+
* @throws Error if handler contains invalid public method names
|
|
10127
|
+
* @throws Error if callbacks is provided but not an object
|
|
9911
10128
|
*/
|
|
9912
10129
|
this.validateShallow = (actionSchema) => {
|
|
9913
10130
|
this.loggerService.log(`actionSchemaService validateShallow`, {
|
|
@@ -9916,21 +10133,30 @@ class ActionSchemaService {
|
|
|
9916
10133
|
if (typeof actionSchema.actionName !== "string") {
|
|
9917
10134
|
throw new Error(`action schema validation failed: missing actionName`);
|
|
9918
10135
|
}
|
|
9919
|
-
if (typeof actionSchema.handler !== "function" &&
|
|
10136
|
+
if (typeof actionSchema.handler !== "function" &&
|
|
10137
|
+
!isObject(actionSchema.handler)) {
|
|
9920
10138
|
throw new Error(`action schema validation failed: handler is not a function or plain object for actionName=${actionSchema.actionName}`);
|
|
9921
10139
|
}
|
|
9922
|
-
if (actionSchema.
|
|
9923
|
-
|
|
10140
|
+
if (typeof actionSchema.handler === "function" && actionSchema.handler.prototype) {
|
|
10141
|
+
VALIDATE_CLASS_METHODS(actionSchema.actionName, actionSchema.handler, this);
|
|
10142
|
+
}
|
|
10143
|
+
if (typeof actionSchema.handler === "object" && actionSchema.handler !== null) {
|
|
10144
|
+
VALIDATE_OBJECT_METHODS(actionSchema.actionName, actionSchema.handler, this);
|
|
10145
|
+
}
|
|
10146
|
+
if (actionSchema.callbacks && !isObject(actionSchema.callbacks)) {
|
|
9924
10147
|
throw new Error(`action schema validation failed: callbacks is not an object for actionName=${actionSchema.actionName}`);
|
|
9925
10148
|
}
|
|
9926
10149
|
};
|
|
9927
10150
|
/**
|
|
9928
10151
|
* Overrides an existing action schema with partial updates.
|
|
9929
10152
|
*
|
|
10153
|
+
* Merges provided partial schema updates with the existing schema.
|
|
10154
|
+
* Useful for modifying handler or callbacks without re-registering the entire schema.
|
|
10155
|
+
*
|
|
9930
10156
|
* @param key - Action name to override
|
|
9931
|
-
* @param value - Partial schema updates
|
|
9932
|
-
* @returns Updated action schema
|
|
9933
|
-
* @throws Error if action name doesn't exist
|
|
10157
|
+
* @param value - Partial schema updates to merge
|
|
10158
|
+
* @returns Updated action schema after override
|
|
10159
|
+
* @throws Error if action name doesn't exist in registry
|
|
9934
10160
|
*/
|
|
9935
10161
|
this.override = (key, value) => {
|
|
9936
10162
|
this.loggerService.log(`actionSchemaService override`, { key });
|
|
@@ -9940,9 +10166,12 @@ class ActionSchemaService {
|
|
|
9940
10166
|
/**
|
|
9941
10167
|
* Retrieves an action schema by name.
|
|
9942
10168
|
*
|
|
9943
|
-
*
|
|
10169
|
+
* Returns the complete action schema configuration including handler and callbacks.
|
|
10170
|
+
* Used internally by ActionConnectionService to instantiate ClientAction instances.
|
|
10171
|
+
*
|
|
10172
|
+
* @param key - Action name identifier
|
|
9944
10173
|
* @returns Action schema configuration
|
|
9945
|
-
* @throws Error if action name doesn't exist
|
|
10174
|
+
* @throws Error if action name doesn't exist in registry
|
|
9946
10175
|
*/
|
|
9947
10176
|
this.get = (key) => {
|
|
9948
10177
|
this.loggerService.log(`actionSchemaService get`, { key });
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -16569,8 +16569,35 @@ declare class RiskSchemaService {
|
|
|
16569
16569
|
/**
|
|
16570
16570
|
* Service for managing action schema registry.
|
|
16571
16571
|
*
|
|
16572
|
+
* Manages registration, validation and retrieval of action schemas.
|
|
16572
16573
|
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
16573
|
-
*
|
|
16574
|
+
* Validates that action handlers only contain allowed public methods
|
|
16575
|
+
* from the IPublicAction interface.
|
|
16576
|
+
*
|
|
16577
|
+
* Key features:
|
|
16578
|
+
* - Type-safe action schema registration
|
|
16579
|
+
* - Method name validation for class and object handlers
|
|
16580
|
+
* - Private method support (methods starting with _ or #)
|
|
16581
|
+
* - Schema override capabilities
|
|
16582
|
+
*
|
|
16583
|
+
* @example
|
|
16584
|
+
* ```typescript
|
|
16585
|
+
* // Register a class-based action
|
|
16586
|
+
* actionSchemaService.register("telegram-notifier", {
|
|
16587
|
+
* actionName: "telegram-notifier",
|
|
16588
|
+
* handler: TelegramNotifierAction,
|
|
16589
|
+
* callbacks: { ... }
|
|
16590
|
+
* });
|
|
16591
|
+
*
|
|
16592
|
+
* // Register an object-based action
|
|
16593
|
+
* actionSchemaService.register("logger", {
|
|
16594
|
+
* actionName: "logger",
|
|
16595
|
+
* handler: {
|
|
16596
|
+
* signal: async (event) => { ... },
|
|
16597
|
+
* dispose: async () => { ... }
|
|
16598
|
+
* }
|
|
16599
|
+
* });
|
|
16600
|
+
* ```
|
|
16574
16601
|
*/
|
|
16575
16602
|
declare class ActionSchemaService {
|
|
16576
16603
|
readonly loggerService: LoggerService;
|
|
@@ -16578,9 +16605,13 @@ declare class ActionSchemaService {
|
|
|
16578
16605
|
/**
|
|
16579
16606
|
* Registers a new action schema.
|
|
16580
16607
|
*
|
|
16581
|
-
*
|
|
16582
|
-
*
|
|
16583
|
-
*
|
|
16608
|
+
* Validates the schema structure and method names before registration.
|
|
16609
|
+
* Throws an error if the action name already exists in the registry.
|
|
16610
|
+
*
|
|
16611
|
+
* @param key - Unique action name identifier
|
|
16612
|
+
* @param value - Action schema configuration with handler and optional callbacks
|
|
16613
|
+
* @throws Error if action name already exists in registry
|
|
16614
|
+
* @throws Error if validation fails (missing required fields, invalid handler, invalid method names)
|
|
16584
16615
|
*/
|
|
16585
16616
|
register: (key: ActionName, value: IActionSchema) => void;
|
|
16586
16617
|
/**
|
|
@@ -16588,28 +16619,36 @@ declare class ActionSchemaService {
|
|
|
16588
16619
|
*
|
|
16589
16620
|
* Performs shallow validation to ensure all required properties exist
|
|
16590
16621
|
* and have correct types before registration in the registry.
|
|
16622
|
+
* Also validates that all public methods in the handler are allowed.
|
|
16591
16623
|
*
|
|
16592
16624
|
* @param actionSchema - Action schema to validate
|
|
16593
16625
|
* @throws Error if actionName is missing or not a string
|
|
16594
|
-
* @throws Error if handler is
|
|
16595
|
-
* @throws Error if
|
|
16626
|
+
* @throws Error if handler is not a function or plain object
|
|
16627
|
+
* @throws Error if handler contains invalid public method names
|
|
16628
|
+
* @throws Error if callbacks is provided but not an object
|
|
16596
16629
|
*/
|
|
16597
16630
|
private validateShallow;
|
|
16598
16631
|
/**
|
|
16599
16632
|
* Overrides an existing action schema with partial updates.
|
|
16600
16633
|
*
|
|
16634
|
+
* Merges provided partial schema updates with the existing schema.
|
|
16635
|
+
* Useful for modifying handler or callbacks without re-registering the entire schema.
|
|
16636
|
+
*
|
|
16601
16637
|
* @param key - Action name to override
|
|
16602
|
-
* @param value - Partial schema updates
|
|
16603
|
-
* @returns Updated action schema
|
|
16604
|
-
* @throws Error if action name doesn't exist
|
|
16638
|
+
* @param value - Partial schema updates to merge
|
|
16639
|
+
* @returns Updated action schema after override
|
|
16640
|
+
* @throws Error if action name doesn't exist in registry
|
|
16605
16641
|
*/
|
|
16606
16642
|
override: (key: ActionName, value: Partial<IActionSchema>) => IActionSchema;
|
|
16607
16643
|
/**
|
|
16608
16644
|
* Retrieves an action schema by name.
|
|
16609
16645
|
*
|
|
16610
|
-
*
|
|
16646
|
+
* Returns the complete action schema configuration including handler and callbacks.
|
|
16647
|
+
* Used internally by ActionConnectionService to instantiate ClientAction instances.
|
|
16648
|
+
*
|
|
16649
|
+
* @param key - Action name identifier
|
|
16611
16650
|
* @returns Action schema configuration
|
|
16612
|
-
* @throws Error if action name doesn't exist
|
|
16651
|
+
* @throws Error if action name doesn't exist in registry
|
|
16613
16652
|
*/
|
|
16614
16653
|
get: (key: ActionName) => IActionSchema;
|
|
16615
16654
|
}
|