chore: Compute default value for jsaction params (#34708)

## Description
This PR adds the values to jsArguments. The logic for this is
- If the value is string then it is kept as is
- For non-strings they are wrapped with `{{ }}` do maintain the data
type integrity when evaluated.

This property is currently not used anywhere in the platform and this is
intended to be used by js modules to identify the default values of
parameters and provide support to alter then in a UI in the app.

This PR also splits `workers/Evaluation/getJSActionForEvalContext.ts` to
override in the EE for modules

PR for https://github.com/appsmithorg/appsmith-ee/pull/4612

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/9919551354>
> Commit: c6ab372477fb3fd2f1ce171729af4fa64ac2a487
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9919551354&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Sat, 13 Jul 2024 12:16:08 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added support for additional node types (`RestElement`,
`ObjectPattern`, `ArrayPattern`) in our AST processing.
- Introduced `addPropertiesToJSObjectCode` function to enhance
JavaScript object property management.

- **Updates**
- Enhanced `myFun2` function with new parameters and default values to
improve flexibility and usage.
- Improved `parseJSObject` function with additional parameters for
better functionality.

- **Tests**
- Added a new test suite for `addPropertiesToJSObjectCode` function to
ensure robust property management in JavaScript objects.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ashit Rath 2024-07-15 21:01:42 +05:30 committed by GitHub
parent bb2f3b1406
commit 70f8777afd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 316 additions and 28 deletions

View File

@ -35,7 +35,11 @@ import type {
JSVarProperty,
JSFunctionProperty,
} from "./src/jsObject";
import { parseJSObject, isJSFunctionProperty } from "./src/jsObject";
import {
parseJSObject,
isJSFunctionProperty,
addPropertiesToJSObjectCode,
} from "./src/jsObject";
// action creator
import {
@ -139,4 +143,5 @@ export {
isFunctionPresent,
PeekOverlayExpressionIdentifier,
getMemberExpressionObjectFromProperty,
addPropertiesToJSObjectCode,
};

View File

@ -16,6 +16,9 @@ export enum NodeTypes {
AssignmentPattern = "AssignmentPattern",
Literal = "Literal",
Property = "Property",
RestElement = "RestElement",
ObjectPattern = "ObjectPattern",
ArrayPattern = "ArrayPattern",
// Declaration - https://github.com/estree/estree/blob/master/es5.md#declarations
FunctionDeclaration = "FunctionDeclaration",
ExportDefaultDeclaration = "ExportDefaultDeclaration",

View File

@ -568,7 +568,7 @@ describe("parseJSObjectWithAST", () => {
it("parse js object with params of all types", () => {
const body = `export default{
myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue()) => {
myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue(), k = "Hello", l = 10, m = null, n = "hello" + 500, o = true, p = () => "arrow function", { o1 = 20, o2 }, [ a1, a2 = 30 ], { k1 = 20, k2 = 40 } = { k1: 500, k2: 600 }, [ g1 = 5, g2 ] = [], ...rest) => {
//use async-await or promises
},
}`;
@ -576,10 +576,12 @@ describe("parseJSObjectWithAST", () => {
const expectedParsedObject = [
{
key: "myFun2",
value:
'async (a, b = Array(1, 2, 3), c = "", d = [], e = this.myVar1, f = {}, g = function () {}, h = Object.assign({}), i = String(), j = storeValue()) => {}',
value: `async (a, b = Array(1, 2, 3), c = \"\", d = [], e = this.myVar1, f = {}, g = function () {}, h = Object.assign({}), i = String(), j = storeValue(), k = \"Hello\", l = 10, m = null, n = \"hello\" + 500, o = true, p = () => \"arrow function\", {o1 = 20, o2}, [a1, a2 = 30], {k1 = 20, k2 = 40} = {
k1: 500,
k2: 600
}, [g1 = 5, g2] = [], ...rest) => {}`,
rawContent:
'myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue()) => {\n' +
'myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue(), k = "Hello", l = 10, m = null, n = "hello" + 500, o = true, p = () => "arrow function", { o1 = 20, o2 }, [ a1, a2 = 30 ], { k1 = 20, k2 = 40 } = { k1: 500, k2: 600 }, [ g1 = 5, g2 ] = [], ...rest) => {\n' +
" //use async-await or promises\n" +
" }",
type: "ArrowFunctionExpression",
@ -595,15 +597,26 @@ describe("parseJSObjectWithAST", () => {
},
arguments: [
{ paramName: "a", defaultValue: undefined },
{ paramName: "b", defaultValue: undefined },
{ paramName: "c", defaultValue: undefined },
{ paramName: "d", defaultValue: undefined },
{ paramName: "e", defaultValue: undefined },
{ paramName: "f", defaultValue: undefined },
{ paramName: "g", defaultValue: undefined },
{ paramName: "h", defaultValue: undefined },
{ paramName: "i", defaultValue: undefined },
{ paramName: "j", defaultValue: undefined },
{ paramName: "b", defaultValue: "{{Array(1,2,3)}}" },
{ paramName: "c", defaultValue: "" },
{ paramName: "d", defaultValue: "{{[]}}" },
{ paramName: "e", defaultValue: "{{this.myVar1}}" },
{ paramName: "f", defaultValue: "{{{}}}" },
{ paramName: "g", defaultValue: "{{function(){}}}" },
{ paramName: "h", defaultValue: "{{Object.assign({})}}" },
{ paramName: "i", defaultValue: "{{String()}}" },
{ paramName: "j", defaultValue: "{{storeValue()}}" },
{ paramName: "k", defaultValue: "Hello" },
{ paramName: "l", defaultValue: "{{10}}" },
{ paramName: "m", defaultValue: "{{null}}" },
{ paramName: "n", defaultValue: '{{"hello" + 500}}' },
{ paramName: "o", defaultValue: "{{true}}" },
{ paramName: "p", defaultValue: '{{() => "arrow function"}}' },
{ paramName: "", defaultValue: "{{{}}}" },
{ paramName: "", defaultValue: "{{[]}}" },
{ paramName: "", defaultValue: undefined },
{ paramName: "", defaultValue: undefined },
{ paramName: "rest", defaultValue: undefined },
],
isMarkedAsync: true,
},

View File

@ -26,7 +26,12 @@ import { generate } from "astring";
*
*/
type Pattern = IdentifierNode | AssignmentPatternNode;
type Pattern =
| IdentifierNode
| AssignmentPatternNode
| ArrayPatternNode
| ObjectPatternNode
| RestElementNode;
type Expression = Node;
export type ArgumentTypes =
| LiteralNode
@ -59,6 +64,31 @@ export interface IdentifierNode extends Node {
name: string;
}
export interface ArrayPatternNode extends Node {
type: NodeTypes.ArrayPattern;
elements: Array<Pattern | null>;
}
export interface AssignmentProperty extends Node {
type: NodeTypes.Property;
key: Expression;
value: Pattern;
kind: "init";
method: false;
shorthand: boolean;
computed: boolean;
}
export interface RestElementNode extends Node {
type: NodeTypes.RestElement;
argument: Pattern;
}
export interface ObjectPatternNode extends Node {
type: NodeTypes.ObjectPattern;
properties: Array<AssignmentProperty | RestElementNode>;
}
//Using this to handle the Variable property refactor
interface RefactorIdentifierNode extends Node {
type: NodeTypes.Identifier;
@ -104,6 +134,7 @@ export interface ObjectExpression extends Expression {
interface AssignmentPatternNode extends Node {
type: NodeTypes.AssignmentPattern;
left: Pattern;
right: ArgumentTypes;
}
// doc: https://github.com/estree/estree/blob/master/es5.md#literal
@ -256,6 +287,18 @@ const isAssignmentPatternNode = (node: Node): node is AssignmentPatternNode => {
return node.type === NodeTypes.AssignmentPattern;
};
export const isArrayPatternNode = (node: Node): node is ArrayPatternNode => {
return node.type === NodeTypes.ArrayPattern;
};
export const isObjectPatternNode = (node: Node): node is ObjectPatternNode => {
return node.type === NodeTypes.ObjectPattern;
};
export const isRestElementNode = (node: Node): node is RestElementNode => {
return node.type === NodeTypes.RestElement;
};
export const isLiteralNode = (node: Node): node is LiteralNode => {
return node.type === NodeTypes.Literal;
};
@ -519,6 +562,7 @@ export const getFunctionalParamsFromNode = (
| FunctionExpressionNode
| ArrowFunctionExpressionNode,
needValue = false,
code = "",
): Set<functionParam> => {
const functionalParams = new Set<functionParam>();
node.params.forEach((paramNode) => {
@ -530,15 +574,50 @@ export const getFunctionalParamsFromNode = (
} else if (isAssignmentPatternNode(paramNode)) {
if (isIdentifierNode(paramNode.left)) {
const paramName = paramNode.left.name;
if (!needValue) {
if (!needValue || !code) {
functionalParams.add({ paramName, defaultValue: undefined });
} else {
// figure out how to get value of paramNode.right for each node type
// currently we don't use params value, hence skipping it
// functionalParams.add({
// defaultValue: paramNode.right.value,
// });
const defaultValueInString = code.slice(
paramNode.right.start,
paramNode.right.end,
);
const defaultValue =
paramNode.right.type === "Literal" &&
typeof paramNode.right.value === "string"
? paramNode.right.value
: `{{${defaultValueInString}}}`;
functionalParams.add({
paramName,
defaultValue,
});
}
} else if (
isObjectPatternNode(paramNode.left) ||
isArrayPatternNode(paramNode.left)
) {
functionalParams.add({
paramName: "",
defaultValue: undefined,
});
}
// The below computations are very basic and can be evolved into nested
// parsing logic to find param and it's default value.
} else if (isObjectPatternNode(paramNode)) {
functionalParams.add({
paramName: "",
defaultValue: `{{{}}}`,
});
} else if (isArrayPatternNode(paramNode)) {
functionalParams.add({
paramName: "",
defaultValue: "{{[]}}",
});
} else if (isRestElementNode(paramNode)) {
if ("name" in paramNode.argument) {
functionalParams.add({
paramName: paramNode.argument.name,
defaultValue: undefined,
});
}
}
});

View File

@ -0,0 +1,137 @@
import { parse } from "acorn";
import { simple } from "acorn-walk";
import { addPropertiesToJSObjectCode } from ".";
describe("addPropertiesToJSObjectCode", () => {
const parseAST = (code: string) =>
parse(code, { sourceType: "module", ecmaVersion: 2020 });
const findProperty = (properties: any[] | undefined, key: string) =>
properties?.find((property) => property.key.name === key);
it("should add new properties to the object", () => {
const body = `
export default {
myVar1: [],
myVar2: {},
myFun1 () {
// write code here
// this.myVar1 = [1,2,3]
},
async myFun2 () {
// use async-await or promises
// await storeValue('varName', 'hello world')
}
}`;
const obj = {
inputs: "Module1.inputs",
newProp: "42",
};
const result = addPropertiesToJSObjectCode(body, obj);
const ast = parseAST(result);
let properties;
simple(ast, {
ExportDefaultDeclaration(node) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
properties = node.declaration.properties;
},
});
const inputsProperty = findProperty(properties, "inputs");
const newPropProperty = findProperty(properties, "newProp");
expect(inputsProperty).toBeDefined();
expect(newPropProperty).toBeDefined();
expect(inputsProperty.value.type).toBe("MemberExpression");
expect(newPropProperty.value.value).toBe(42);
});
it("should replace existing properties", () => {
const body = `
export default {
myVar1: [],
myVar2: {},
inputs: 'oldValue',
myFun1 () {
// write code here
// this.myVar1 = [1,2,3]
},
async myFun2 () {
// use async-await or promises
// await storeValue('varName', 'hello world')
}
}`;
const obj = {
inputs: "Module1.inputs",
myVar1: "[1, 2, 3]",
};
const result = addPropertiesToJSObjectCode(body, obj);
const ast = parseAST(result);
let properties;
simple(ast, {
ExportDefaultDeclaration(node) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
properties = node.declaration.properties;
},
});
const inputsProperty = findProperty(properties, "inputs");
const myVar1Property = findProperty(properties, "myVar1");
expect(inputsProperty).toBeDefined();
expect(myVar1Property).toBeDefined();
expect(inputsProperty.value.type).toBe("MemberExpression");
expect(myVar1Property.value.type).toBe("ArrayExpression");
expect(
myVar1Property.value.elements.map((e: { value: any }) => e.value),
).toEqual([1, 2, 3]);
});
it("should handle empty object input without errors", () => {
const body = `export default {
myVar1: [],
myVar2: {},
myFun1() {
},
async myFun2() {
}
};`;
const obj = {};
const result = addPropertiesToJSObjectCode(body, obj);
expect(result).toBe(body);
});
it("should handle empty string input without errors", () => {
const body = ``;
const obj = {
inputs: "Module1.inputs",
};
const result = addPropertiesToJSObjectCode(body, obj);
expect(result).toBe(body);
});
it("should handle missing export default declaration gracefully", () => {
const body = `const myVar1 = [];
const myVar2 = {};
function myFun1() {
}
async function myFun2() {
}`;
const obj = {
inputs: "Module1.inputs",
};
const result = addPropertiesToJSObjectCode(body, obj);
expect(result).toEqual(body);
});
});

View File

@ -1,4 +1,4 @@
import type { Node } from "acorn";
import { parseExpressionAt, type Node } from "acorn";
import { simple } from "acorn-walk";
import type {
IdentifierNode,
@ -15,8 +15,8 @@ import {
import { generate } from "astring";
import type { functionParam } from "../index";
import { getFunctionalParamsFromNode, isPropertyAFunctionNode } from "../index";
import { SourceType } from "../../index";
import { attachComments } from "escodegen";
import { ECMA_VERSION, SourceType } from "../../index";
import escodegen, { attachComments } from "escodegen";
import { extractContentByPosition } from "../utils";
const jsObjectVariableName =
@ -52,6 +52,10 @@ export type JSVarProperty = BaseJSProperty;
export type TParsedJSProperty = JSVarProperty | JSFunctionProperty;
interface Property extends PropertyNode {
key: IdentifierNode;
}
export const isJSFunctionProperty = (
t: TParsedJSProperty,
): t is JSFunctionProperty => {
@ -122,9 +126,7 @@ export const parseJSObject = (code: string) => {
};
if (isPropertyAFunctionNode(node.value)) {
// if in future we need default values of each param, we could implement that in getFunctionalParamsFromNode
// currently we don't consume it anywhere hence avoiding to calculate that.
const params = getFunctionalParamsFromNode(node.value);
const params = getFunctionalParamsFromNode(node.value, true, code);
property = {
...property,
arguments: [...params],
@ -137,3 +139,51 @@ export const parseJSObject = (code: string) => {
return { parsedObject: [...parsedObjectProperties], success: true };
};
export const addPropertiesToJSObjectCode = (
code: string,
obj: Record<string, string>,
) => {
try {
const ast = getAST(code, { sourceType: "module" });
simple(ast, {
ExportDefaultDeclaration(node: any) {
const properties: Property[] = node?.declaration?.properties;
Object.entries(obj).forEach(([key, value]) => {
// Check if a property with the same name already exists
const existingPropertyIndex = properties.findIndex(
(property) => property.key.name === key,
);
const astValue = parseExpressionAt(value, 0, {
ecmaVersion: ECMA_VERSION,
});
// Create a new property
const newProperty = {
type: "Property",
key: { type: "Identifier", name: key },
value: astValue,
kind: "init",
method: false,
shorthand: false,
computed: false,
} as unknown as Property;
if (existingPropertyIndex >= 0) {
// Replace the existing property
properties[existingPropertyIndex] = newProperty;
} else {
// Add the new property
properties.push(newProperty);
}
});
},
});
return escodegen.generate(ast);
} catch (e) {
return code;
}
};

View File

@ -1,6 +1,6 @@
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import type { DataTreeEntity } from "entities/DataTree/dataTreeTypes";
import { getJSActionForEvalContext } from "workers/Evaluation/getJSActionForEvalContext";
import { getJSActionForEvalContext } from "@appsmith/workers/Evaluation/getJSActionForEvalContext";
export const getEntityForEvalContextMap: Record<
string,

View File

@ -0,0 +1 @@
export * from "ce/workers/Evaluation/getJSActionForEvalContext";