From 28ff8097394dd9226fc3a327df8788a19a5fc216 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Mon, 25 Aug 2025 15:57:34 +0530 Subject: [PATCH] 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 * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../config/BasicConfig.ts | 15 ++++----- packages/lib/raqb/jsonLogic.test.ts | 33 +++++++++++++++++++ packages/lib/raqb/jsonLogic.ts | 14 ++++++++ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts b/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts index 5220118c0f..21689593f9 100644 --- a/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts +++ b/packages/app-store/routing-forms/components/react-awesome-query-builder/config/BasicConfig.ts @@ -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", diff --git a/packages/lib/raqb/jsonLogic.test.ts b/packages/lib/raqb/jsonLogic.test.ts index d2a6dca1b8..72a0f80a62 100644 --- a/packages/lib/raqb/jsonLogic.test.ts +++ b/packages/lib/raqb/jsonLogic.test.ts @@ -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); + }); + }); }); diff --git a/packages/lib/raqb/jsonLogic.ts b/packages/lib/raqb/jsonLogic.ts index 5627a29df1..f3edbb0372 100644 --- a/packages/lib/raqb/jsonLogic.ts +++ b/packages/lib/raqb/jsonLogic.ts @@ -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;