feat: add startsWith operator to routing forms (#23026)

* feat: add startsWith operator to routing forms

- Implement startsWith operator in BasicConfig.ts for RAQB
- Add custom JsonLogic operation for startsWith evaluation
- Add startsWith support to jsonLogicToPrisma for reporting
- Supports both form field response routing and attribute routing
- Uses case-insensitive matching via normalize function

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* test: add comprehensive unit tests for startsWith operator

- Add tests for operator availability in FormFieldsBaseConfig and AttributesBaseConfig
- Add tests for JsonLogic configuration validation
- Add tests for text widget operator integration
- All 15 tests pass including new startsWith operator tests

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* test: move startsWith operator tests to jsonLogic.test.ts

- Add comprehensive startsWith tests to packages/lib/raqb/jsonLogic.test.ts
- Remove startsWith tests from packages/app-store/routing-forms/__tests__/config.test.ts
- Tests verify case-insensitive matching, edge cases, and falsy value handling
- All tests pass: jsonLogic.test.ts (18 passed) and config.test.ts (12 passed)

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: remove unused startsWith operator from jsonLogicToPrisma.ts

- Remove startsWith entry from OPERATOR_MAP in jsonLogicToPrisma.ts
- File is currently unused so this cleanup keeps PR focused
- Core startsWith functionality remains intact in jsonLogic.ts and BasicConfig.ts

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* test: add comprehensive edge case tests for startsWith operator

- Add test cases for non-string first argument (haystack) handling
- Cover null, undefined, numbers, booleans, arrays, and objects
- Update startsWith implementation to safely handle non-string inputs
- Ensure operation returns false without throwing runtime errors
- All tests now pass: 19 passed | 1 skipped

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: improve startsWith operation with type guards and String.prototype.startsWith

- Change argument types from string to unknown for better type safety
- Add type guards to return false when either operand is not a string
- Use String.prototype.startsWith instead of indexOf for cleaner implementation
- Keep existing empty-second check and normalize function calls
- All tests continue to pass: 19 passed | 1 skipped

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Hariom Balhara
2025-08-25 15:57:34 +05:30
committed by GitHub
co-authored by Devin AI
parent 8e73421094
commit 28ff809739
3 changed files with 53 additions and 9 deletions
@@ -96,15 +96,12 @@ const operators: Operators = {
labelForFormat: "Not Contains",
valueSources: ["value"],
},
/**
* Not supported with JSONLogic. Implement them and add these back -> https://github.com/jwadhams/json-logic-js/issues/81
*/
// starts_with: {
// label: "Starts with",
// labelForFormat: "Starts with",
// jsonLogic: undefined, // not supported
// valueSources: ["value"],
// },
starts_with: {
label: "Starts with",
labelForFormat: "Starts with",
jsonLogic: "starts_with",
valueSources: ["value"],
},
// ends_with: {
// label: "Ends with",
// labelForFormat: "Ends with",
+33
View File
@@ -225,4 +225,37 @@ describe("jsonLogic", () => {
).toBe(false);
});
});
describe("starts_with operation", () => {
it("should return true if string starts with the given substring case-insensitively", () => {
expect(jsonLogic.apply({ starts_with: ["Hello World", "hello"] })).toBe(true);
expect(jsonLogic.apply({ starts_with: ["HELLO WORLD", "hello"] })).toBe(true);
expect(jsonLogic.apply({ starts_with: ["hello", "HELLO"] })).toBe(true);
});
it("should return false if string does not start with the given substring", () => {
expect(jsonLogic.apply({ starts_with: ["Hello World", "world"] })).toBe(false);
expect(jsonLogic.apply({ starts_with: ["Hello World", "hi"] })).toBe(false);
});
it("should return false if the second argument is falsy", () => {
expect(jsonLogic.apply({ starts_with: ["Hello World", ""] })).toBe(false);
expect(jsonLogic.apply({ starts_with: ["Hello World", null] })).toBe(false);
expect(jsonLogic.apply({ starts_with: ["Hello World", false] })).toBe(false);
});
it("should handle edge cases", () => {
expect(jsonLogic.apply({ starts_with: ["", "hello"] })).toBe(false);
expect(jsonLogic.apply({ starts_with: ["hello", "hello world"] })).toBe(false);
});
it("should handle non-string first argument (haystack) safely", () => {
expect(jsonLogic.apply({ starts_with: [null, "x"] })).toBe(false);
expect(jsonLogic.apply({ starts_with: [undefined, "x"] })).toBe(false);
expect(jsonLogic.apply({ starts_with: [123, "1"] })).toBe(false);
expect(jsonLogic.apply({ starts_with: [true, "t"] })).toBe(false);
expect(jsonLogic.apply({ starts_with: [[], ""] })).toBe(false);
expect(jsonLogic.apply({ starts_with: [{}, ""] })).toBe(false);
});
});
});
+14
View File
@@ -50,4 +50,18 @@ jsonLogic.add_operation("in", function (a: string, b: string | string[]) {
return second.indexOf(first) !== -1;
});
/**
* Short Text/Long Text "starts with" uses it
*/
jsonLogic.add_operation("starts_with", function (a: unknown, b: unknown) {
if (typeof a !== "string" || typeof b !== "string") return false;
const first = normalize(a);
const second = normalize(b);
if (!second) return false;
return first.startsWith(second);
});
export default jsonLogic;