PromucFlow_constructor/app/client/src/plugins/Linting/handlers/lintService.ts
Ayush Pahwa 5fdc0a1ef9
feat: workflows assign request on browser (#31159)
## 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 -->
2024-03-04 19:56:47 +05:30

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();