From 95a0b19e41069ea7e20a913d561299874593e2c5 Mon Sep 17 00:00:00 2001 From: Cesar Parra Date: Sun, 23 Aug 2026 09:58:52 -0400 Subject: [PATCH 1/2] TOJSON function for JSON serialization --- cirrus.toml | 34 -- cirrus.yaml | 66 ++++ docs/src/app/docs/functions/page.md | 303 +++++++++--------- .../editor/lwc/functions/functions-src.js | 281 ++++++++-------- .../src/interpreter/std-lib/DataFunctions.cls | 41 ++- .../std-functions/DataFunctionsTest.cls | 35 ++ package-lock.json | 138 +++++++- package.json | 2 +- sfdx-project_packaging.json | 5 +- 9 files changed, 577 insertions(+), 328 deletions(-) delete mode 100644 cirrus.toml create mode 100644 cirrus.yaml diff --git a/cirrus.toml b/cirrus.toml deleted file mode 100644 index c92a86a9..00000000 --- a/cirrus.toml +++ /dev/null @@ -1,34 +0,0 @@ -[[orgs]] -name = "Expression" -definitionFile = "config/dev-communities.json" - -[commands] -ls = "sf org list --skip-connection-status" -deploy = "sf project deploy start" -assign-perms = "sf org assign permset -n Expression_Admin" -test = "sf apex test run --test-level RunLocalTests --wait 20" -docs = "apexdocs markdown" -generate-library-docs = "bun scripts/generate-library-docs.mjs" -prep-package = "bun scripts/prepare-for-packaging.mjs" -run-package = "cirrus package create -p Expression -t minor -x -c --promote -w 60" -post-package = "bun scripts/post-packaging.mjs" -deploy-conference-community = "sf project deploy start -d unpackaged/examples/expression-conf/main" -import-data = "sf data import tree -p unpackaged/examples/expression-conf/data/data-plan.json" - -[flow.start-dev] -description = "Starts a new development environment" -steps = [ - { type = "create_scratch", org = "Expression" }, - { type = "command", name = "deploy" }, - { type = "command", name = "assign-perms" }, - { type = "command", name = "deploy-conference-community" }, - { type = "command", name = "import-data" }, -] - -[flow.package] -description = "Create a managed package version for Expression" -steps = [ - { type = "command", name = "prep-package" }, - { type = "command", name = "run-package" }, - { type = "command", name = "post-package" }, -] diff --git a/cirrus.yaml b/cirrus.yaml new file mode 100644 index 00000000..e2b71a56 --- /dev/null +++ b/cirrus.yaml @@ -0,0 +1,66 @@ +# yaml-language-server: $schema=https://cesarparra.github.io/cirrus/schema/v1/cirrus.schema.json +# +# Every verb this repo has, and what each one needs before it can run. +# +# Commands are not run through a shell: `&&`, pipes and $VARIABLES would reach the program as +# arguments. A sequence is a flow. + +schemaVersion: 1 + +defaultOrg: Expression + +orgs: + Expression: + definitionFile: config/dev-communities.json + +commands: + ls: + run: sf org list --skip-connection-status + + deploy: + run: sf project deploy start + + assign-perms: + run: sf org assign permset -n Expression_Admin + + test: + run: sf apex test run --test-level RunLocalTests --wait 20 + + docs: + run: apexdocs markdown + + generate-library-docs: + description: Rebuild the function reference and the editor's autocomplete list from the ApexDoc. + run: bun scripts/generate-library-docs.mjs + + prep-package: + run: bun scripts/prepare-for-packaging.mjs + + run-package: + run: cirrus package create -p Expression -t minor -x -c --promote -w 60 + + post-package: + run: bun scripts/post-packaging.mjs + + deploy-conference-community: + run: sf project deploy start -d unpackaged/examples/expression-conf/main + + import-data: + run: sf data import tree -p unpackaged/examples/expression-conf/data/data-plan.json + +flows: + start-dev: + description: Starts a new development environment + steps: + - createScratch: Expression + - command: deploy + - command: assign-perms + - command: deploy-conference-community + - command: import-data + + package: + description: Create a managed package version for Expression + steps: + - command: prep-package + - command: run-package + - command: post-package diff --git a/docs/src/app/docs/functions/page.md b/docs/src/app/docs/functions/page.md index 8dfe8d46..aa420e93 100644 --- a/docs/src/app/docs/functions/page.md +++ b/docs/src/app/docs/functions/page.md @@ -6,56 +6,6 @@ nextjs: description: Functions overview. --- -## Data - -### LET - -Allows you to define custom variables that can be used in the expression. -Accepts 2 arguments: a map of variables to define and the expression to evaluate. -The map keys should be the variable names prefixed with `$`. - -``` -LET({ "$a": 1, "$b": 2 }, $a + $b) // 3 -``` - -### PARSEJSON - -Parses a JSON string into a usable map/object structure. -Accepts 1 argument: the JSON string to parse. - -``` -PARSEJSON(Contact.Custom_JSON_Field__c) // Parses JSON from a field -``` - -### PRINT - -Allows you to print a value to the playground console. -Accepts 1 argument: the value to print. - -``` -PRINT("Hello World") -``` - -### RAWQUERY - -Allows you to run a raw query against the database. -Accepts 1 argument: the query to run. - -``` -RAWQUERY("SELECT Id, Name FROM Account LIMIT 10") -``` - -### TRANSFORM - -Transforms any input using the provided expression. -Provides a special variable `$source` in the inner expression that contains the original input. - -Accepts 2 arguments: the input to transform and the expression to evaluate. - -``` -TRANSFORM("Hello World", UPPER($source)) // "HELLO WORLD" -``` - ## Collection ### AGGREGATEGROUPS @@ -401,6 +351,65 @@ Accepts 2 arguments: List of objects and an expression to evaluate. WHERE([1, 2, 3], $current > 1) ``` +## Data + +### LET + +Allows you to define custom variables that can be used in the expression. +Accepts 2 arguments: a map of variables to define and the expression to evaluate. +The map keys should be the variable names prefixed with `$`. + +``` +LET({ "$a": 1, "$b": 2 }, $a + $b) // 3 +``` + +### PARSEJSON + +Parses a JSON string into a usable map/object structure. +Accepts 1 argument: the JSON string to parse. + +``` +PARSEJSON(Contact.Custom_JSON_Field__c) // Parses JSON from a field +``` + +### PRINT + +Allows you to print a value to the playground console. +Accepts 1 argument: the value to print. + +``` +PRINT("Hello World") +``` + +### RAWQUERY + +Allows you to run a raw query against the database. +Accepts 1 argument: the query to run. + +``` +RAWQUERY("SELECT Id, Name FROM Account LIMIT 10") +``` + +### TOJSON + +Serializes any value into a JSON string. The inverse of `PARSEJSON`. +Accepts 1 argument: the value to serialize. + +``` +Contacts -> MAP([Name, Email]) -> TOJSON() // A table of values, as JSON +``` + +### TRANSFORM + +Transforms any input using the provided expression. +Provides a special variable `$source` in the inner expression that contains the original input. + +Accepts 2 arguments: the input to transform and the expression to evaluate. + +``` +TRANSFORM("Hello World", UPPER($source)) // "HELLO WORLD" +``` + ## Date and Time ### ADDDAYS @@ -795,6 +804,103 @@ OR(true, false, true) // true OR(false, false, false) // false ``` +## Math + +### ABS + +Returns the absolute value of a number. + +Accepts 1 argument: the number to evaluate. + +``` +ABS(-1) // 1 +``` + +### CEILING + +Returns the smallest integer greater than or equal to the specified number. + +Accepts 1 argument: the number to evaluate. + +``` +CEILING(1.5) // 2 +``` + +### FLOOR + +Returns the largest integer less than or equal to the specified number. + +Accepts 1 argument: the number to evaluate. + +``` +FLOOR(1.5) // 1 +``` + +### FORMATNUMBER + +Formats a number with comma as thousand separator. + +Accepts 1 or 2 arguments: the number to format and optionally the number of decimal places. + +``` +FORMATNUMBER(20000.53) // "20,000.53" +FORMATNUMBER(20000.53, 1) // "20,000.5" +``` + +### MAX + +Returns the largest value in a list of numbers. + +Accepts either a list of numbers as a single argument, or multiple numerical arguments. + +``` +MAX(1, 2, 3) // 3 +MAX([1, 2, 3]) // 3 +``` + +### MIN + +Returns the smallest value in a list of numbers. + +Accepts either a list of numbers as a single argument, or multiple numerical arguments. + +``` +MIN(1, 2, 3) // 1 +MIN([1, 2, 3]) // 1 +``` + +### MOD + +Returns the remainder of one number divided by another. + +Accepts 2 arguments: the dividend and the divisor. + +``` +MOD(5, 2) // 1 +``` + +### ROUND + +Returns a rounded number. Optionally specify the number of decimal places to round to. + +Accepts 1 or 2 arguments: the number to round and optionally the number of decimal places to round to. + +``` +ROUND(1.234) // 1 +ROUND(1.234, 2) // 1.23 +``` + +### TRUNC + +Returns a truncated number. Optionally specify the number of decimal places to truncate to. + +Accepts 1 or 2 arguments: the number to truncate and optionally the number of decimal places to truncate to. + +``` +TRUNC(1.234) // 1 +TRUNC(1.234, 2) // 1.23 +``` + ## String ### BEGINS @@ -1057,100 +1163,3 @@ Accepts 1 argument: the text to convert. VALUE("123") // 123 ``` -## Math - -### ABS - -Returns the absolute value of a number. - -Accepts 1 argument: the number to evaluate. - -``` -ABS(-1) // 1 -``` - -### CEILING - -Returns the smallest integer greater than or equal to the specified number. - -Accepts 1 argument: the number to evaluate. - -``` -CEILING(1.5) // 2 -``` - -### FLOOR - -Returns the largest integer less than or equal to the specified number. - -Accepts 1 argument: the number to evaluate. - -``` -FLOOR(1.5) // 1 -``` - -### FORMATNUMBER - -Formats a number with comma as thousand separator. - -Accepts 1 or 2 arguments: the number to format and optionally the number of decimal places. - -``` -FORMATNUMBER(20000.53) // "20,000.53" -FORMATNUMBER(20000.53, 1) // "20,000.5" -``` - -### MAX - -Returns the largest value in a list of numbers. - -Accepts either a list of numbers as a single argument, or multiple numerical arguments. - -``` -MAX(1, 2, 3) // 3 -MAX([1, 2, 3]) // 3 -``` - -### MIN - -Returns the smallest value in a list of numbers. - -Accepts either a list of numbers as a single argument, or multiple numerical arguments. - -``` -MIN(1, 2, 3) // 1 -MIN([1, 2, 3]) // 1 -``` - -### MOD - -Returns the remainder of one number divided by another. - -Accepts 2 arguments: the dividend and the divisor. - -``` -MOD(5, 2) // 1 -``` - -### ROUND - -Returns a rounded number. Optionally specify the number of decimal places to round to. - -Accepts 1 or 2 arguments: the number to round and optionally the number of decimal places to round to. - -``` -ROUND(1.234) // 1 -ROUND(1.234, 2) // 1.23 -``` - -### TRUNC - -Returns a truncated number. Optionally specify the number of decimal places to truncate to. - -Accepts 1 or 2 arguments: the number to truncate and optionally the number of decimal places to truncate to. - -``` -TRUNC(1.234) // 1 -TRUNC(1.234, 2) // 1.23 -``` - diff --git a/expression-src/main/editor/lwc/functions/functions-src.js b/expression-src/main/editor/lwc/functions/functions-src.js index 3860b924..440fa3ba 100644 --- a/expression-src/main/editor/lwc/functions/functions-src.js +++ b/expression-src/main/editor/lwc/functions/functions-src.js @@ -1,54 +1,4 @@ export const data = [ - { - "category": "Data", - "values": [ - { - "name": "LET", - "autoCompleteValue": "LET(", - "description": "Allows you to define custom variables that can be used in the expression.
Accepts 2 arguments: a map of variables to define and the expression to evaluate.
The map keys should be the variable names prefixed with `$`.", - "examples": [ - "LET({ \"$a\": 1, \"$b\": 2 }, $a + $b) // 3" - ], - "icon": "utility:data_mapping" - }, - { - "name": "PARSEJSON", - "autoCompleteValue": "PARSEJSON(", - "description": "Parses a JSON string into a usable map/object structure.
Accepts 1 argument: the JSON string to parse.", - "examples": [ - "PARSEJSON(Contact.Custom_JSON_Field__c) // Parses JSON from a field" - ], - "icon": "utility:data_mapping" - }, - { - "name": "PRINT", - "autoCompleteValue": "PRINT(", - "description": "Allows you to print a value to the playground console.
Accepts 1 argument: the value to print.", - "examples": [ - "PRINT(\"Hello World\")" - ], - "icon": "utility:data_mapping" - }, - { - "name": "RAWQUERY", - "autoCompleteValue": "RAWQUERY(", - "description": "Allows you to run a raw query against the database.
Accepts 1 argument: the query to run.", - "examples": [ - "RAWQUERY(\"SELECT Id, Name FROM Account LIMIT 10\")" - ], - "icon": "utility:data_mapping" - }, - { - "name": "TRANSFORM", - "autoCompleteValue": "TRANSFORM(", - "description": "Transforms any input using the provided expression.
Provides a special variable `$source` in the inner expression that contains the original input.

Accepts 2 arguments: the input to transform and the expression to evaluate.", - "examples": [ - "TRANSFORM(\"Hello World\", UPPER($source)) // \"HELLO WORLD\"" - ], - "icon": "utility:data_mapping" - } - ] - }, { "category": "Collection", "values": [ @@ -360,6 +310,65 @@ export const data = [ } ] }, + { + "category": "Data", + "values": [ + { + "name": "LET", + "autoCompleteValue": "LET(", + "description": "Allows you to define custom variables that can be used in the expression.
Accepts 2 arguments: a map of variables to define and the expression to evaluate.
The map keys should be the variable names prefixed with `$`.", + "examples": [ + "LET({ \"$a\": 1, \"$b\": 2 }, $a + $b) // 3" + ], + "icon": "utility:data_mapping" + }, + { + "name": "PARSEJSON", + "autoCompleteValue": "PARSEJSON(", + "description": "Parses a JSON string into a usable map/object structure.
Accepts 1 argument: the JSON string to parse.", + "examples": [ + "PARSEJSON(Contact.Custom_JSON_Field__c) // Parses JSON from a field" + ], + "icon": "utility:data_mapping" + }, + { + "name": "PRINT", + "autoCompleteValue": "PRINT(", + "description": "Allows you to print a value to the playground console.
Accepts 1 argument: the value to print.", + "examples": [ + "PRINT(\"Hello World\")" + ], + "icon": "utility:data_mapping" + }, + { + "name": "RAWQUERY", + "autoCompleteValue": "RAWQUERY(", + "description": "Allows you to run a raw query against the database.
Accepts 1 argument: the query to run.", + "examples": [ + "RAWQUERY(\"SELECT Id, Name FROM Account LIMIT 10\")" + ], + "icon": "utility:data_mapping" + }, + { + "name": "TOJSON", + "autoCompleteValue": "TOJSON(", + "description": "Serializes any value into a JSON string. The inverse of `PARSEJSON`.
Accepts 1 argument: the value to serialize.", + "examples": [ + "Contacts -> MAP([Name, Email]) -> TOJSON() // A table of values, as JSON" + ], + "icon": "utility:data_mapping" + }, + { + "name": "TRANSFORM", + "autoCompleteValue": "TRANSFORM(", + "description": "Transforms any input using the provided expression.
Provides a special variable `$source` in the inner expression that contains the original input.

Accepts 2 arguments: the input to transform and the expression to evaluate.", + "examples": [ + "TRANSFORM(\"Hello World\", UPPER($source)) // \"HELLO WORLD\"" + ], + "icon": "utility:data_mapping" + } + ] + }, { "category": "Date and Time", "values": [ @@ -708,6 +717,92 @@ export const data = [ } ] }, + { + "category": "Math", + "values": [ + { + "name": "ABS", + "autoCompleteValue": "ABS(", + "description": "Returns the absolute value of a number.

Accepts 1 argument: the number to evaluate.", + "examples": [ + "ABS(-1) // 1" + ], + "icon": "utility:advanced_function" + }, + { + "name": "CEILING", + "autoCompleteValue": "CEILING(", + "description": "Returns the smallest integer greater than or equal to the specified number.

Accepts 1 argument: the number to evaluate.", + "examples": [ + "CEILING(1.5) // 2" + ], + "icon": "utility:advanced_function" + }, + { + "name": "FLOOR", + "autoCompleteValue": "FLOOR(", + "description": "Returns the largest integer less than or equal to the specified number.

Accepts 1 argument: the number to evaluate.", + "examples": [ + "FLOOR(1.5) // 1" + ], + "icon": "utility:advanced_function" + }, + { + "name": "FORMATNUMBER", + "autoCompleteValue": "FORMATNUMBER(", + "description": "Formats a number with comma as thousand separator.

Accepts 1 or 2 arguments: the number to format and optionally the number of decimal places.", + "examples": [ + "FORMATNUMBER(20000.53) // \"20,000.53\"\nFORMATNUMBER(20000.53, 1) // \"20,000.5\"" + ], + "icon": "utility:advanced_function" + }, + { + "name": "MAX", + "autoCompleteValue": "MAX(", + "description": "Returns the largest value in a list of numbers.

Accepts either a list of numbers as a single argument, or multiple numerical arguments.", + "examples": [ + "MAX(1, 2, 3) // 3\nMAX([1, 2, 3]) // 3" + ], + "icon": "utility:advanced_function" + }, + { + "name": "MIN", + "autoCompleteValue": "MIN(", + "description": "Returns the smallest value in a list of numbers.

Accepts either a list of numbers as a single argument, or multiple numerical arguments.", + "examples": [ + "MIN(1, 2, 3) // 1\nMIN([1, 2, 3]) // 1" + ], + "icon": "utility:advanced_function" + }, + { + "name": "MOD", + "autoCompleteValue": "MOD(", + "description": "Returns the remainder of one number divided by another.

Accepts 2 arguments: the dividend and the divisor.", + "examples": [ + "MOD(5, 2) // 1" + ], + "icon": "utility:advanced_function" + }, + { + "name": "ROUND", + "autoCompleteValue": "ROUND(", + "description": "Returns a rounded number. Optionally specify the number of decimal places to round to.

Accepts 1 or 2 arguments: the number to round and optionally the number of decimal places to round to.", + "examples": [ + "ROUND(1.234) // 1\nROUND(1.234, 2) // 1.23" + ], + "icon": "utility:advanced_function" + }, + { + "name": "TRUNC", + "autoCompleteValue": "TRUNC(", + "description": "Returns a truncated number. Optionally specify the number of decimal places to truncate to.

Accepts 1 or 2 arguments: the number to truncate and optionally the number of decimal places to truncate to.", + "examples": [ + "TRUNC(1.234) // 1\nTRUNC(1.234, 2) // 1.23" + ], + "icon": "utility:advanced_function" + } + ] + }, { "category": "String", "values": [ @@ -919,91 +1014,5 @@ export const data = [ "icon": "utility:text" } ] - }, - { - "category": "Math", - "values": [ - { - "name": "ABS", - "autoCompleteValue": "ABS(", - "description": "Returns the absolute value of a number.

Accepts 1 argument: the number to evaluate.", - "examples": [ - "ABS(-1) // 1" - ], - "icon": "utility:advanced_function" - }, - { - "name": "CEILING", - "autoCompleteValue": "CEILING(", - "description": "Returns the smallest integer greater than or equal to the specified number.

Accepts 1 argument: the number to evaluate.", - "examples": [ - "CEILING(1.5) // 2" - ], - "icon": "utility:advanced_function" - }, - { - "name": "FLOOR", - "autoCompleteValue": "FLOOR(", - "description": "Returns the largest integer less than or equal to the specified number.

Accepts 1 argument: the number to evaluate.", - "examples": [ - "FLOOR(1.5) // 1" - ], - "icon": "utility:advanced_function" - }, - { - "name": "FORMATNUMBER", - "autoCompleteValue": "FORMATNUMBER(", - "description": "Formats a number with comma as thousand separator.

Accepts 1 or 2 arguments: the number to format and optionally the number of decimal places.", - "examples": [ - "FORMATNUMBER(20000.53) // \"20,000.53\"\nFORMATNUMBER(20000.53, 1) // \"20,000.5\"" - ], - "icon": "utility:advanced_function" - }, - { - "name": "MAX", - "autoCompleteValue": "MAX(", - "description": "Returns the largest value in a list of numbers.

Accepts either a list of numbers as a single argument, or multiple numerical arguments.", - "examples": [ - "MAX(1, 2, 3) // 3\nMAX([1, 2, 3]) // 3" - ], - "icon": "utility:advanced_function" - }, - { - "name": "MIN", - "autoCompleteValue": "MIN(", - "description": "Returns the smallest value in a list of numbers.

Accepts either a list of numbers as a single argument, or multiple numerical arguments.", - "examples": [ - "MIN(1, 2, 3) // 1\nMIN([1, 2, 3]) // 1" - ], - "icon": "utility:advanced_function" - }, - { - "name": "MOD", - "autoCompleteValue": "MOD(", - "description": "Returns the remainder of one number divided by another.

Accepts 2 arguments: the dividend and the divisor.", - "examples": [ - "MOD(5, 2) // 1" - ], - "icon": "utility:advanced_function" - }, - { - "name": "ROUND", - "autoCompleteValue": "ROUND(", - "description": "Returns a rounded number. Optionally specify the number of decimal places to round to.

Accepts 1 or 2 arguments: the number to round and optionally the number of decimal places to round to.", - "examples": [ - "ROUND(1.234) // 1\nROUND(1.234, 2) // 1.23" - ], - "icon": "utility:advanced_function" - }, - { - "name": "TRUNC", - "autoCompleteValue": "TRUNC(", - "description": "Returns a truncated number. Optionally specify the number of decimal places to truncate to.

Accepts 1 or 2 arguments: the number to truncate and optionally the number of decimal places to truncate to.", - "examples": [ - "TRUNC(1.234) // 1\nTRUNC(1.234, 2) // 1.23" - ], - "icon": "utility:advanced_function" - } - ] } ]; \ No newline at end of file diff --git a/expression-src/main/src/interpreter/std-lib/DataFunctions.cls b/expression-src/main/src/interpreter/std-lib/DataFunctions.cls index 8a9d3a86..46549512 100644 --- a/expression-src/main/src/interpreter/std-lib/DataFunctions.cls +++ b/expression-src/main/src/interpreter/std-lib/DataFunctions.cls @@ -10,7 +10,8 @@ public with sharing class DataFunctions { 'LET' => new LET(), 'RAWQUERY' => new RawQuery(), 'PRINT' => new PrintFn(), - 'PARSEJSON' => new ParseJsonFn() + 'PARSEJSON' => new ParseJsonFn(), + 'TOJSON' => new ToJsonFn() }; public class DataFunctionsProvider implements FunctionProvider { @@ -203,4 +204,42 @@ public with sharing class DataFunctions { return Arity.exactly(1); } } + + /** + * @description Serializes any value into a JSON string. The inverse of `PARSEJSON`. + * Accepts 1 argument: the value to serialize. + * @function TOJSON + * @example TOJSON([1, "two", true]) // "[1,\"two\",true]" + * @example Contacts -> MAP([Name, Email]) -> TOJSON() // A table of values, as JSON + */ + private class ToJsonFn extends StandardFunction { + public override Object call(List arguments) { + return JSON.serialize(serializable(evaluate(arguments.get(0)))); + } + + // A map literal evaluates to a Map, whose keys JSON.serialize refuses. + private Object serializable(Object value) { + if (value instanceof Map) { + Map withStringKeys = new Map(); + for (Object key : ((Map) value).keySet()) { + withStringKeys.put(String.valueOf(key), serializable(((Map) value).get(key))); + } + return withStringKeys; + } + + if (value instanceof List) { + List serialized = new List(); + for (Object item : (List) value) { + serialized.add(serializable(item)); + } + return serialized; + } + + return value; + } + + public override Arity getArity() { + return Arity.exactly(1); + } + } } diff --git a/expression-src/spec/language/std-functions/DataFunctionsTest.cls b/expression-src/spec/language/std-functions/DataFunctionsTest.cls index 338d09ad..9a6b0e6c 100644 --- a/expression-src/spec/language/std-functions/DataFunctionsTest.cls +++ b/expression-src/spec/language/std-functions/DataFunctionsTest.cls @@ -82,4 +82,39 @@ private class DataFunctionsTest { Assert.areEqual('HTML', skills[2]); } + @IsTest + private static void toJsonSerializesAList() { + String formula = 'TOJSON([1, "two", true])'; + Object result = Evaluator.run(formula); + Assert.areEqual('[1,"two",true]', result); + } + + @IsTest + private static void toJsonSerializesAListOfListsBuiltByMap() { + List accounts = new List{ + new Account(Name = 'First', NumberOfEmployees = 1), + new Account(Name = 'Second', NumberOfEmployees = 2) + }; + insert accounts; + + String formula = 'QUERY(Account(orderBy: Name)[Name, NumberOfEmployees]) -> MAP([Name, NumberOfEmployees]) -> TOJSON()'; + Object result = Evaluator.run(formula); + + Assert.areEqual('[["First",1],["Second",2]]', result); + } + + @IsTest + private static void toJsonSerializesAnEmptyList() { + String formula = '[] -> TOJSON()'; + Object result = Evaluator.run(formula); + Assert.areEqual('[]', result); + } + + @IsTest + private static void toJsonRoundTripsThroughParseJson() { + String formula = 'GET(PARSEJSON(TOJSON({"name": "John"})), "name")'; + Object result = Evaluator.run(formula); + Assert.areEqual('John', result); + } + } diff --git a/package-lock.json b/package-lock.json index d24b89cc..68f6aa80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "license": "MIT", "devDependencies": { "@cparra/apex-reflection": "^2.4.1", - "@cparra/apexdocs": "^3.17.0", + "@cparra/apexdocs": "^3.23.1", "@tailwindcss/forms": "^0.5.6", "@types/node": "^20.10.2", "js-yaml": "^4.1.0", @@ -62,20 +62,20 @@ "license": "ISC" }, "node_modules/@cparra/apexdocs": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@cparra/apexdocs/-/apexdocs-3.17.0.tgz", - "integrity": "sha512-ONSpQADzBj7gjC62tshhVJ9DA3Uk2dCQF98B1vNqkxXRk//Mu/LEoPmuZZKxD1IwC34TRksQ2L9XVW4sFhfG3g==", + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/@cparra/apexdocs/-/apexdocs-3.23.1.tgz", + "integrity": "sha512-zpPj8o3l2co9j7wnssPv8qNSEe8cuwPAP7xckPXNgt0ADpMe2r0yMqfM1i69HiJFqZByLgpPmrQQeqHIofpwzQ==", "dev": true, "license": "MIT", "dependencies": { - "@cparra/apex-reflection": "2.22.0", + "@cparra/apex-reflection": "3.1.0", "@salesforce/source-deploy-retrieve": "^12.20.1", "@types/js-yaml": "^4.0.9", "@types/yargs": "^17.0.32", "chalk": "^4.1.2", "cosmiconfig": "^9.0.0", "cosmiconfig-typescript-loader": "^5.0.0", - "fast-xml-parser": "^4.4.0", + "fast-xml-parser": "^5.5.9", "fp-ts": "^2.16.8", "handlebars": "^4.7.8", "js-yaml": "4.1.1", @@ -86,7 +86,40 @@ "apexdocs": "dist/cli/generate.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" + } + }, + "node_modules/@cparra/apexdocs/node_modules/@cparra/apex-reflection": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@cparra/apex-reflection/-/apex-reflection-3.1.0.tgz", + "integrity": "sha512-IomkjSD5W79pcJeXDpgaGQOXZGuHUxX66XWNwgy0VDwze3RHglbqk4t9kC02x9a6ddcaxAUwbjbyVhrNfGySMA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cparra/apexdocs/node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" } }, "node_modules/@cparra/apexdocs/node_modules/minimatch": { @@ -105,6 +138,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@cparra/apexdocs/node_modules/strnum": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz", + "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.0" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -341,6 +390,19 @@ "tslib": "2" } }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -697,6 +759,19 @@ "node": ">= 8" } }, + "node_modules/anynum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", + "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -1695,6 +1770,23 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, "node_modules/fast-xml-parser": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", @@ -3084,6 +3176,22 @@ "tslib": "^2.0.3" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4633,6 +4741,22 @@ "dev": true, "license": "ISC" }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index 0052db2d..08320b98 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "homepage": "https://github.com/cesarParra/formula-evaluator#readme", "devDependencies": { "@cparra/apex-reflection": "^2.4.1", - "@cparra/apexdocs": "^3.17.0", + "@cparra/apexdocs": "^3.23.1", "@tailwindcss/forms": "^0.5.6", "@types/node": "^20.10.2", "js-yaml": "^4.1.0", diff --git a/sfdx-project_packaging.json b/sfdx-project_packaging.json index 4c55a421..9ae77b05 100644 --- a/sfdx-project_packaging.json +++ b/sfdx-project_packaging.json @@ -3,7 +3,7 @@ { "package": "Expression", "versionName": "Version 1.36", - "versionNumber": "1.51.0.NEXT", + "versionNumber": "1.52.0.NEXT", "path": "expression-src", "default": false, "versionDescription": "Expression core language", @@ -80,6 +80,7 @@ "Expression@1.48.0-1": "04tRb0000045WPxIAM", "Expression@1.49.0-1": "04tRb000004f533IAA", "Expression@1.50.0-1": "04tRb000004f56HIAQ", - "Expression@1.51.0-1": "04tRb0000059vllIAA" + "Expression@1.51.0-1": "04tRb0000059vllIAA", + "Expression@1.52.0-1": "04tRb000005Y0txIAC" } } \ No newline at end of file From eaabce132c86662a3da405c260704614b4a5d3c0 Mon Sep 17 00:00:00 2001 From: Cesar Parra Date: Sun, 23 Aug 2026 10:07:19 -0400 Subject: [PATCH 2/2] Package upgrade --- docs/public/packages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/public/packages.json b/docs/public/packages.json index 7a21a3da..c2ed99ad 100644 --- a/docs/public/packages.json +++ b/docs/public/packages.json @@ -1,4 +1,4 @@ { - "packageId": "04tRb0000059vllIAA", + "packageId": "04tRb000005Y0txIAC", "componentPackageId": "04tRb0000012Mv8IAE" }