## Description We are allowing users to have write workflows assign request queries in the JS object editor for the workflows editor. This means, users should get autocomplete for `appsmith.workflows.assignRequest` and the arguments required should be shown in the autocomplete also. To enable this, following changes have been made 1) Add empty workflows object to the appsmith function type in store and and add ee only type definitions to the `EntityDefinitions` for appsmith namespace. 2) Replace entityFunctions as static variable and use a function which appends EE entity functions. 3) Added types for ee only functions 4) Retain the `isMainJsObject` flag for the js file when updates happen. #### PR fixes following issue(s) Fixes # (issue number) #### Type of change - New feature (non-breaking change which adds functionality) ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] JUnit - [ ] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced new autocomplete definitions for enhanced code editor suggestions. - Added support for categorizing entities in the Explorer panel using `GROUP_TYPES` for improved organization and navigation. - **Refactor** - Updated the action helpers and reducers to support new properties and ensure state consistency. - Improved the structure for autocomplete helper functions, facilitating easier extension and maintenance. - **Chores** - Established foundational code for future development with empty enums and placeholder functions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
356 lines
12 KiB
TypeScript
356 lines
12 KiB
TypeScript
import { get, intersection, isEmpty, uniq } from "lodash";
|
|
import {
|
|
convertPathToString,
|
|
getAllPaths,
|
|
getEntityNameAndPropertyPath,
|
|
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
|
import { AppsmithFunctionsWithFields } from "components/editorComponents/ActionCreator/constants";
|
|
import { PathUtils } from "plugins/Linting/utils/pathUtils";
|
|
import { extractReferencesFromPath } from "@appsmith/plugins/Linting/utils/getEntityDependencies";
|
|
import { groupDifferencesByType } from "plugins/Linting/utils/groupDifferencesByType";
|
|
import type {
|
|
LintTreeRequestPayload,
|
|
LintTreeResponse,
|
|
} from "plugins/Linting/types";
|
|
import { getLintErrorsFromTree } from "plugins/Linting/lintTree";
|
|
import type {
|
|
TJSPropertiesState,
|
|
TJSpropertyState,
|
|
} from "workers/Evaluation/JSObject/jsPropertiesState";
|
|
import { isJSEntity } from "@appsmith/plugins/Linting/lib/entity";
|
|
import DependencyMap from "entities/DependencyMap";
|
|
import {
|
|
LintEntityTree,
|
|
type EntityTree,
|
|
} from "plugins/Linting/lib/entity/EntityTree";
|
|
import { getEntityFunctions } from "@appsmith/workers/Evaluation/fns";
|
|
|
|
class LintService {
|
|
cachedEntityTree: EntityTree | null;
|
|
dependencyMap: DependencyMap = new DependencyMap();
|
|
constructor() {
|
|
this.cachedEntityTree = null;
|
|
if (isEmpty(this.cachedEntityTree)) {
|
|
this.dependencyMap = new DependencyMap();
|
|
this.dependencyMap.addNodes(
|
|
convertArrayToObject(AppsmithFunctionsWithFields),
|
|
);
|
|
}
|
|
}
|
|
|
|
lintTree = (payload: LintTreeRequestPayload) => {
|
|
const {
|
|
cloudHosting,
|
|
configTree,
|
|
forceLinting = false,
|
|
unevalTree: unEvalTree,
|
|
} = payload;
|
|
|
|
const entityTree = new LintEntityTree(unEvalTree, configTree);
|
|
|
|
const { asyncJSFunctionsInDataFields, pathsToLint } =
|
|
isEmpty(this.cachedEntityTree) || forceLinting
|
|
? this.lintFirstTree(entityTree)
|
|
: this.lintUpdatedTree(entityTree);
|
|
|
|
const jsEntities = entityTree.getEntities().filter(isJSEntity);
|
|
const jsPropertiesState: TJSPropertiesState = {};
|
|
for (const jsEntity of jsEntities) {
|
|
const rawEntity = jsEntity.getRawEntity();
|
|
const config = jsEntity.getConfig();
|
|
if (!jsEntity.entityParser) continue;
|
|
const { parsedEntityConfig } = jsEntity.entityParser.parse(
|
|
rawEntity,
|
|
config,
|
|
);
|
|
jsPropertiesState[jsEntity.getName()] = parsedEntityConfig as Record<
|
|
string,
|
|
TJSpropertyState
|
|
>;
|
|
}
|
|
|
|
const lintTreeResponse: LintTreeResponse = {
|
|
errors: {},
|
|
lintedJSPaths: [],
|
|
jsPropertiesState,
|
|
};
|
|
try {
|
|
const { errors: lintErrors, lintedJSPaths } = getLintErrorsFromTree({
|
|
pathsToLint,
|
|
unEvalTree: this.cachedEntityTree?.getRawTree() || {},
|
|
jsPropertiesState,
|
|
cloudHosting,
|
|
asyncJSFunctionsInDataFields,
|
|
|
|
configTree,
|
|
});
|
|
|
|
lintTreeResponse.errors = lintErrors;
|
|
lintTreeResponse.lintedJSPaths = lintedJSPaths;
|
|
} catch (e) {}
|
|
return lintTreeResponse;
|
|
};
|
|
|
|
private lintFirstTree = (entityTree: EntityTree) => {
|
|
const pathsToLint: Array<string> = [];
|
|
const allNodes: Record<string, true> = entityTree.getAllPaths();
|
|
const asyncJSFunctionsInDataFields: Record<string, string[]> = {};
|
|
this.dependencyMap.addNodes(allNodes);
|
|
|
|
const entities = entityTree.getEntities();
|
|
|
|
for (const entity of entities) {
|
|
const dynamicPaths = PathUtils.getDynamicPaths(entity);
|
|
for (const path of dynamicPaths) {
|
|
const references = extractReferencesFromPath(entity, path, allNodes);
|
|
this.dependencyMap.addDependency(path, references);
|
|
pathsToLint.push(path);
|
|
}
|
|
}
|
|
const asyncEntityActions = AppsmithFunctionsWithFields.concat(
|
|
getAllEntityActions(entityTree),
|
|
);
|
|
const asyncFns = entities
|
|
.filter(isJSEntity)
|
|
.flatMap((e) => e.getFns())
|
|
.filter(
|
|
(fn) =>
|
|
fn.isMarkedAsync ||
|
|
this.dependencyMap.isRelated(fn.name, asyncEntityActions),
|
|
)
|
|
.map((fn) => fn.name);
|
|
|
|
for (const asyncFn of asyncFns) {
|
|
const nodesThatDependOnAsyncFn =
|
|
this.dependencyMap.getDependents(asyncFn);
|
|
const dataPathsThatDependOnAsyncFn = filterDataPaths(
|
|
nodesThatDependOnAsyncFn,
|
|
entityTree,
|
|
);
|
|
if (isEmpty(dataPathsThatDependOnAsyncFn)) continue;
|
|
asyncJSFunctionsInDataFields[asyncFn] = dataPathsThatDependOnAsyncFn;
|
|
}
|
|
|
|
this.cachedEntityTree = entityTree;
|
|
return {
|
|
pathsToLint,
|
|
asyncJSFunctionsInDataFields,
|
|
};
|
|
};
|
|
|
|
private lintUpdatedTree(entityTree: EntityTree) {
|
|
const asyncJSFunctionsInDataFields: Record<string, string[]> = {};
|
|
const pathsToLint: string[] = [];
|
|
const NOOP = {
|
|
pathsToLint: [],
|
|
asyncJSFunctionsInDataFields,
|
|
};
|
|
const entityTreeDiff =
|
|
this.cachedEntityTree?.computeDifferences(entityTree);
|
|
if (!entityTreeDiff) return NOOP;
|
|
|
|
const { additions, deletions, edits } =
|
|
groupDifferencesByType(entityTreeDiff);
|
|
|
|
const allNodes = getAllPaths(entityTree.getRawTree());
|
|
|
|
const updatedPathsDetails: Record<
|
|
string,
|
|
{
|
|
previousDependencies: string[];
|
|
currentDependencies: string[];
|
|
updateType: "EDIT" | "ADD" | "DELETE";
|
|
}
|
|
> = {};
|
|
|
|
for (const edit of edits) {
|
|
const pathString = convertPathToString(edit?.path || []);
|
|
if (!pathString) continue;
|
|
const { entityName } = getEntityNameAndPropertyPath(pathString);
|
|
const entity = entityTree.getEntityByName(entityName);
|
|
if (!entity) continue;
|
|
const dynamicPaths = PathUtils.getDynamicPaths(entity);
|
|
if (!dynamicPaths.includes(pathString)) {
|
|
if (!dynamicPaths.some((p) => pathString.startsWith(p))) continue;
|
|
}
|
|
|
|
const previousDependencies =
|
|
this.dependencyMap.getDirectDependencies(pathString);
|
|
const references = extractReferencesFromPath(
|
|
entity,
|
|
pathString,
|
|
allNodes,
|
|
);
|
|
this.dependencyMap.addDependency(pathString, references);
|
|
pathsToLint.push(pathString);
|
|
|
|
updatedPathsDetails[pathString] = {
|
|
previousDependencies,
|
|
currentDependencies: references,
|
|
updateType: "EDIT",
|
|
};
|
|
}
|
|
|
|
for (const addition of additions) {
|
|
const pathString = convertPathToString(addition?.path || []);
|
|
if (!pathString) continue;
|
|
const { entityName } = getEntityNameAndPropertyPath(pathString);
|
|
if (!entityName) continue;
|
|
const entity = entityTree.getEntityByName(entityName);
|
|
if (!entity) continue;
|
|
const allAddedPaths = PathUtils.getAllPaths({
|
|
[pathString]: get(entityTree.getRawTree(), pathString),
|
|
});
|
|
this.dependencyMap.addNodes(allAddedPaths);
|
|
for (const path of Object.keys(allAddedPaths)) {
|
|
const previousDependencies =
|
|
this.dependencyMap.getDirectDependencies(path);
|
|
const references = extractReferencesFromPath(entity, path, allNodes);
|
|
if (PathUtils.isDynamicLeaf(entity, path)) {
|
|
this.dependencyMap.addDependency(path, references);
|
|
pathsToLint.push(path);
|
|
}
|
|
const incomingDeps = this.dependencyMap.getDependents(path);
|
|
pathsToLint.push(...incomingDeps);
|
|
|
|
updatedPathsDetails[path] = {
|
|
previousDependencies,
|
|
currentDependencies: references,
|
|
updateType: "ADD",
|
|
};
|
|
}
|
|
}
|
|
for (const deletion of deletions) {
|
|
const pathString = convertPathToString(deletion?.path || []);
|
|
if (!pathString) continue;
|
|
const { entityName } = getEntityNameAndPropertyPath(pathString);
|
|
if (!entityName) continue;
|
|
const entity = this.cachedEntityTree?.getEntityByName(entityName); // Use previous tree in a DELETE EVENT
|
|
if (!entity) continue;
|
|
|
|
const allDeletedPaths = PathUtils.getAllPaths({
|
|
[pathString]: get(this.cachedEntityTree?.getRawTree(), pathString),
|
|
});
|
|
|
|
for (const path of Object.keys(allDeletedPaths)) {
|
|
const previousDependencies =
|
|
this.dependencyMap.getDirectDependencies(path);
|
|
|
|
updatedPathsDetails[path] = {
|
|
previousDependencies,
|
|
currentDependencies: [],
|
|
updateType: "DELETE",
|
|
};
|
|
|
|
const incomingDeps = this.dependencyMap.getDependents(path);
|
|
pathsToLint.push(...incomingDeps);
|
|
}
|
|
this.dependencyMap.removeNodes(allDeletedPaths);
|
|
}
|
|
|
|
// generate async functions only after dependencyMap update is complete
|
|
const asyncEntityActions = AppsmithFunctionsWithFields.concat(
|
|
getAllEntityActions(entityTree),
|
|
);
|
|
const asyncFns = entityTree
|
|
.getEntities()
|
|
.filter(isJSEntity)
|
|
.flatMap((e) => e.getFns())
|
|
.filter(
|
|
(fn) =>
|
|
fn.isMarkedAsync ||
|
|
this.dependencyMap.isRelated(fn.name, asyncEntityActions),
|
|
)
|
|
.map((fn) => fn.name);
|
|
|
|
// generate asyncFunctionsBoundToSyncFields
|
|
|
|
for (const [updatedPath, details] of Object.entries(updatedPathsDetails)) {
|
|
const { currentDependencies, previousDependencies, updateType } = details;
|
|
const { entityName } = getEntityNameAndPropertyPath(updatedPath);
|
|
if (!entityName) continue;
|
|
// Use cached entityTree in a delete event
|
|
const entityTreeToUse =
|
|
updateType === "DELETE" ? this.cachedEntityTree : entityTree;
|
|
const entity = entityTreeToUse?.getEntityByName(entityName);
|
|
if (!entity) continue;
|
|
|
|
if (isJSEntity(entity) && asyncFns.includes(updatedPath)) {
|
|
const nodesThatDependOnAsyncFn =
|
|
this.dependencyMap.getDependents(updatedPath);
|
|
const dataPathsThatDependOnAsyncFn = filterDataPaths(
|
|
nodesThatDependOnAsyncFn,
|
|
entityTree,
|
|
);
|
|
if (!isEmpty(dataPathsThatDependOnAsyncFn)) {
|
|
asyncJSFunctionsInDataFields[updatedPath] =
|
|
dataPathsThatDependOnAsyncFn;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const isDataPath = PathUtils.isDataPath(updatedPath, entity);
|
|
if (!isDataPath) continue;
|
|
|
|
const asyncDeps = intersection(asyncFns, currentDependencies);
|
|
const prevAsyncDeps = intersection(asyncFns, previousDependencies);
|
|
|
|
for (const asyncFn of asyncDeps) {
|
|
const nodesThatDependOnAsyncFn =
|
|
this.dependencyMap.getDependents(asyncFn);
|
|
const dataPathsThatDependOnAsyncFn = filterDataPaths(
|
|
nodesThatDependOnAsyncFn,
|
|
entityTree,
|
|
);
|
|
if (isEmpty(dataPathsThatDependOnAsyncFn)) continue;
|
|
asyncJSFunctionsInDataFields[asyncFn] = dataPathsThatDependOnAsyncFn;
|
|
}
|
|
pathsToLint.push(...asyncDeps, ...prevAsyncDeps);
|
|
}
|
|
|
|
this.cachedEntityTree = entityTree;
|
|
return {
|
|
pathsToLint: uniq(pathsToLint),
|
|
entityTree,
|
|
asyncJSFunctionsInDataFields,
|
|
};
|
|
}
|
|
}
|
|
|
|
function convertArrayToObject(arr: string[]) {
|
|
return arr.reduce(
|
|
(acc, item) => {
|
|
return { ...acc, [item]: true } as const;
|
|
},
|
|
{} as Record<string, true>,
|
|
);
|
|
}
|
|
|
|
function filterDataPaths(paths: string[], entityTree: EntityTree) {
|
|
const dataPaths: string[] = [];
|
|
for (const path of paths) {
|
|
const { entityName } = getEntityNameAndPropertyPath(path);
|
|
const entity = entityTree.getEntityByName(entityName);
|
|
if (!entity || !PathUtils.isDataPath(path, entity)) continue;
|
|
dataPaths.push(path);
|
|
}
|
|
return dataPaths;
|
|
}
|
|
function getAllEntityActions(entityTree: EntityTree) {
|
|
const allEntityActions = new Set<string>();
|
|
for (const [entityName, entity] of Object.entries(entityTree.getRawTree())) {
|
|
for (const entityFnDescription of getEntityFunctions()) {
|
|
if (entityFnDescription.qualifier(entity)) {
|
|
const fullPath = `${
|
|
entityFnDescription.path ||
|
|
`${entityName}.${entityFnDescription.name}`
|
|
}`;
|
|
allEntityActions.add(fullPath);
|
|
}
|
|
}
|
|
}
|
|
return [...allEntityActions];
|
|
}
|
|
|
|
export const lintService = new LintService();
|