diff --git a/app/client/public/index.html b/app/client/public/index.html index f44efa34ec..0492727c0b 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -3,6 +3,7 @@ + diff --git a/app/client/public/shims/realms-shim.umd.min.js b/app/client/public/shims/realms-shim.umd.min.js new file mode 100644 index 0000000000..2b74c0b9cc --- /dev/null +++ b/app/client/public/shims/realms-shim.umd.min.js @@ -0,0 +1,10 @@ +(function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):(a=a||self,a.Realm=b())})(this,function(){'use strict';function a(a,b=void 0){const c=`please report internal shim error: ${a}`;console.error(c),b&&(console.error(`${b}`),console.error(`${b.stack}`));debugger;throw c}function b(b,c){b||a(c)}function c(a){let b=`'use strict'; (${a})`;return b=b.replace(/\(0,\s*_[0-9a-fA-F]{3}\u200D\.e\)/g,"(0, eval)"),b=b.replace(/_[0-9a-fA-F]{3}\u200D\.g\./g,""),b=b.replace(/cov_[^+]+\+\+[;,]/g,""),b}function d(a,b){const{callAndWrapError:c}=a,{initRootRealm:d,initCompartment:e,getRealmGlobal:f,realmEvaluate:g}=b,{create:h,defineProperties:i}=Object;class j{constructor(){throw new TypeError("Realm is not a constructor")}static makeRootRealm(b={}){const e=h(j.prototype);return c(d,[a,e,b]),e}static makeCompartment(b={}){const d=h(j.prototype);return c(e,[a,d,b]),d}get global(){return c(f,[this])}evaluate(a,b,d={}){return c(g,[this,a,b,d])}}return i(j,{toString:{value:()=>"function Realm() { [shim code] }",writable:!1,enumerable:!1,configurable:!0}}),i(j.prototype,{toString:{value:()=>"[object Realm]",writable:!1,enumerable:!1,configurable:!0}}),j}function e(a){function c(c,e,f,g){for(const h of c){const c=G(a,h);c&&(b("value"in c,`unexpected accessor on global property: ${h}`),d[h]={value:c.value,writable:e,enumerable:f,configurable:g})}}const d={};return c(V,!1,!1,!1),c(W,!1,!1,!1),c(X,!0,!1,!0),d}function f(){function a(a){if(a===void 0||null===a)throw new TypeError(`can't convert undefined or null to object`);return Object(a)}function b(a){return"symbol"==typeof a?a:`${a}`}function c(a,b){if("function"!=typeof a)throw TypeError(`invalid ${b} usage`);return a}const{defineProperty:d,defineProperties:e,getOwnPropertyDescriptor:f,getPrototypeOf:g,prototype:h}=Object;try{(0,h.__lookupGetter__)("x")}catch(a){return}e(h,{__defineGetter__:{value:function(b,e){const f=a(this);d(f,b,{get:c(e,"getter"),enumerable:!0,configurable:!0})}},__defineSetter__:{value:function(b,e){const f=a(this);d(f,b,{set:c(e,"setter"),enumerable:!0,configurable:!0})}},__lookupGetter__:{value:function(c){let d=a(this);c=b(c);let e;for(;d&&!(e=f(d,c));)d=g(d);return e&&e.get}},__lookupSetter__:{value:function(c){let d=a(this);c=b(c);let e;for(;d&&!(e=f(d,c));)d=g(d);return e&&e.set}}})}function g(){function a(a,e){let f;try{f=(0,eval)(e)}catch(a){if(a instanceof SyntaxError)return;throw a}const g=c(f),h=function(){throw new TypeError("Not available")};b(h,{name:{value:a}}),b(g,{constructor:{value:h}}),b(h,{prototype:{value:g}}),h!==Function.prototype.constructor&&d(h,Function.prototype.constructor)}const{defineProperties:b,getPrototypeOf:c,setPrototypeOf:d}=Object;a("Function","(function(){})"),a("GeneratorFunction","(function*(){})"),a("AsyncFunction","(async function(){})"),a("AsyncGeneratorFunction","(async function*(){})")}function h(){const a=new Function("try {return this===global}catch(e){return false}")();if(!a)return;const b=require("vm"),c=b.runInNewContext(Z);return c}function i(){if("undefined"!=typeof document){const a=document.createElement("iframe");a.style.display="none",document.body.appendChild(a);const b=a.contentWindow.eval(Y);return b}}function j(a,b=[]){const c=e(a),d=a.eval,f=a.Function,g=d(B)();return E({unsafeGlobal:a,sharedGlobalDescs:c,unsafeEval:d,unsafeFunction:f,callAndWrapError:g,allShims:b})}function k(a){const b=$(),c=j(b,a),{unsafeEval:d}=c;return d(_)(),d(aa)(),c}function l(a,b={}){const c=I(a),d=P(c,c=>{if(c in b)return!1;if("eval"===c||ca.has(c)||!T(ba,c))return!1;const d=G(a,c);return!1===d.configurable&&!1===d.writable&&O(d,"value")});return d}function m(a){const b=a.search(ha);if(-1!==b){const c=a.slice(0,b).split("\n").length;throw new SyntaxError(`possible html comment syntax rejected around line ${c}`)}}function n(a){const b=a.search(ia);if(-1!==b){const c=a.slice(0,b).split("\n").length;throw new SyntaxError(`possible import expression rejected around line ${c}`)}}function o(a){const b=a.search(ja);if(-1!==b){const c=a.slice(0,b).split("\n").length;throw new SyntaxError(`possible direct eval expression rejected around line ${c}`)}}function p(a){m(a),n(a),o(a)}function q(a){return 0===a.length?"":`const {${R(a,",")}} = this;`}function r(a,b){const{unsafeFunction:c}=a,d=q(b);return c(` + with (arguments[0]) { + ${d} + return function() { + 'use strict'; + return eval(arguments[0]); + }; + } + `)}function s(b,c,d,e){const{unsafeEval:f}=b,g=f(ga);return function(h={},i={}){const j=i.transforms||[],k=S(j,d||[],[ka]);return function(d){let i={src:d,endowments:h};i=g(i,k);const j=l(c,i.endowments),m=l(i.endowments),n=S(j,m),o=r(b,n),p=f(da)(b,c,i.endowments,e),q=Proxy.revocable({},p),s=q.proxy,t=L(o,c,[s]);p.useUnsafeEvaluator=!0;let u;try{return L(t,c,[i.src])}catch(a){throw u=a,a}finally{p.useUnsafeEvaluator&&(q.revoke(),a("handler did not revoke useUnsafeEvaluator",u))}}}}function t(a,c){const{unsafeEval:d,unsafeFunction:e}=a,f=d(ea)(a,c);return b(J(f).constructor!==Function,"hide Function"),b(J(f).constructor!==e,"hide unsafeFunction"),f}function u(a){return(b,c,d={})=>a(c,d)(b)}function v(a,c){const{unsafeGlobal:d,unsafeEval:e,unsafeFunction:f}=a,g=e(fa)(a,function(...a){const b=`${Q(a)||""}`;let e=`${R(a,",")}`;if(!T(/^[\w\s,]*$/,e))throw new SyntaxError("shim limitation: Function arg must be simple ASCII identifiers, possibly separated by commas: no default values, pattern matches, or non-ASCII parameter names");if(new f(b),U(e,")"))throw new d.SyntaxError("shim limitation: Function arg string contains parenthesis");0(c,...d)=>b(a,c,d),d=c(Map.prototype.get),e=c(Set.prototype.has),f=new Map([["EvalError",EvalError],["RangeError",RangeError],["ReferenceError",ReferenceError],["SyntaxError",SyntaxError],["TypeError",TypeError],["URIError",URIError]]),g=new Set([EvalError.prototype,RangeError.prototype,ReferenceError.prototype,SyntaxError.prototype,TypeError.prototype,URIError.prototype,Error.prototype]);return function(c,h){try{return b(c,void 0,h)}catch(b){if(Object(b)!==b)throw b;if(e(g,a(b)))throw b;let c,h,i;try{c=`${b.name}`,h=`${b.message}`,i=`${b.stack||h}`}catch(a){throw new Error("unknown error")}const j=d(f,c)||Error;try{throw new j(h)}catch(a){throw a.stack=i,a}}}}),{assign:C,create:D,freeze:E,defineProperties:F,getOwnPropertyDescriptor:G,getOwnPropertyDescriptors:H,getOwnPropertyNames:I,getPrototypeOf:J,setPrototypeOf:K}=Object,{apply:L,ownKeys:M}=Reflect,N=a=>(b,...c)=>L(a,b,c),O=N(Object.prototype.hasOwnProperty),P=N(Array.prototype.filter),Q=N(Array.prototype.pop),R=N(Array.prototype.join),S=N(Array.prototype.concat),T=N(RegExp.prototype.test),U=N(String.prototype.includes),V=["Infinity","NaN","undefined"],W=["isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","Array","ArrayBuffer","Boolean","DataView","EvalError","Float32Array","Float64Array","Int8Array","Int16Array","Int32Array","Map","Number","Object","RangeError","ReferenceError","Set","String","Symbol","SyntaxError","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","URIError","WeakMap","WeakSet","JSON","Math","Reflect","escape","unescape"],X=["Date","Error","Promise","Proxy","RegExp","Intl"],Y="'use strict'; this",Z=`(0, eval)("'use strict'; this")`,$=()=>{const a=i(),b=h();if(!a&&!b||a&&b)throw new Error("unexpected platform, unable to create Realm");return a||b},_=c(f),aa=c(g),ba=/^[a-zA-Z_$][\w$]*$/,ca=new Set(["await","break","case","catch","class","const","continue","debugger","default","delete","do","else","export","extends","finally","for","function","if","import","in","instanceof","new","return","super","switch","this","throw","try","typeof","var","void","while","with","yield","let","static","enum","implements","package","protected","interface","private","public","await","null","true","false","this","arguments"]),da=c(function(a,b,c={},d=!1){const{unsafeGlobal:e,unsafeEval:f}=a,{freeze:g,getOwnPropertyDescriptor:h}=Object,{get:i,set:j}=Reflect,k=new Proxy(g({}),{get(a,b){throw new TypeError(`unexpected scope handler trap called: ${b+""}`)}});return{__proto__:k,useUnsafeEvaluator:!1,get(a,d){return"symbol"==typeof d?void 0:"eval"===d&&!0===this.useUnsafeEvaluator?(this.useUnsafeEvaluator=!1,f):d in c?i(c,d,b):i(b,d)},set(a,d,e){if(d in c){const a=h(c,d);return"value"in a?j(c,d,e):j(c,d,e,b)}return j(b,d,e)},has(a,f){return!!d||!!("eval"===f||f in c||f in b||f in e)},getPrototypeOf(){return null}}}),ea=c(function(a,b){const{callAndWrapError:c}=a,{defineProperties:d}=Object,e={eval(){return c(b,arguments)}}.eval;return d(e,{toString:{value:()=>`function ${"eval"}() { [shim code] }`,writable:!1,enumerable:!1,configurable:!0}}),e}),fa=c(function(a,b){const{callAndWrapError:c,unsafeFunction:d}=a,{defineProperties:e}=Object,f=function(){return c(b,arguments)};return e(f,{prototype:{value:d.prototype},toString:{value:()=>"function Function() { [shim code] }",writable:!1,enumerable:!1,configurable:!0}}),f}),ga=c(function(a,b){const{create:c,getOwnPropertyDescriptors:d}=Object,{apply:e}=Reflect,f=(a=>(b,...c)=>e(a,b,c))(Array.prototype.reduce);return a={src:`${a.src}`,endowments:c(null,d(a.endowments))},a=f(b,(a,b)=>b.rewrite?b.rewrite(a):a,a),a={src:`${a.src}`,endowments:c(null,d(a.endowments))},a}),ha=/(?:)/,ia=/\bimport\s*(?:\(|\/[/*])/,ja=/\beval\s*(?:\(|\/[/*])/,ka={rewrite(a){return p(a.src),a}},la=new WeakMap,ma={initRootRealm:function(a,b,c){const{shims:d,transforms:e,sloppyGlobals:f}=c,g=S(a.allShims,d),h=k(g),{unsafeEval:i}=h,j=i(A)(h,ma);h.sharedGlobalDescs.Realm={value:j,writable:!0,configurable:!0};const l=z(h,e,f),{safeEvalWhichTakesEndowments:m}=l;for(const d of g)m(d);x(b,l)},initCompartment:function(a,b,c={}){const{transforms:d,sloppyGlobals:e}=c,f=z(a,d,e);x(b,f)},getRealmGlobal:function(a){const{safeGlobal:b}=w(a);return b},realmEvaluate:function(a,b,c={},d={}){const{safeEvalWhichTakesEndowments:e}=w(a);return e(b,c,d)}},na=function(){const a=eval,b=a(Y);return f(),g(),j(b)}(),oa=d(na,ma);return oa}); +//# sourceMappingURL=realms-shim.umd.min.js.map diff --git a/app/client/src/constants/ActionConstants.tsx b/app/client/src/constants/ActionConstants.tsx index fe3fd47b29..aad107e36d 100644 --- a/app/client/src/constants/ActionConstants.tsx +++ b/app/client/src/constants/ActionConstants.tsx @@ -27,7 +27,7 @@ export const PropertyPaneActionDropdownOptions: DropdownOption[] = [ // { label: "Run Query", value: "QUERY" }, ]; -export interface ActionPayload { +export interface BaseActionPayload { actionId: string; actionType: ActionType; contextParams: Record; @@ -35,34 +35,42 @@ export interface ActionPayload { onError?: ActionPayload[]; } +export type ActionPayload = + | NavigateActionPayload + | SetValueActionPayload + | ExecuteJSActionPayload + | DownloadDataActionPayload + | SetValueActionPayload; + export type NavigationType = "NEW_TAB" | "INLINE"; -export interface NavigateActionPayload extends ActionPayload { +export interface NavigateActionPayload extends BaseActionPayload { pageUrl: string; navigationType: NavigationType; } -export interface ShowAlertActionPayload extends ActionPayload { +export interface ShowAlertActionPayload extends BaseActionPayload { header: string; message: string; alertType: AlertType; intent: MessageIntent; } -export interface SetValueActionPayload extends ActionPayload { +export interface SetValueActionPayload extends BaseActionPayload { header: string; message: string; alertType: AlertType; intent: MessageIntent; } -export interface ExecuteJSActionPayload extends ActionPayload { +export interface ExecuteJSActionPayload extends BaseActionPayload { jsFunctionId: string; + jsFunction: string; } export type DownloadFiletype = "CSV" | "XLS" | "JSON" | "TXT"; -export interface DownloadDataActionPayload extends ActionPayload { +export interface DownloadDataActionPayload extends BaseActionPayload { data: JSON; fileName: string; fileType: DownloadFiletype; diff --git a/app/client/src/constants/BindingsConstants.ts b/app/client/src/constants/BindingsConstants.ts index e3fc912a00..d1f091312c 100644 --- a/app/client/src/constants/BindingsConstants.ts +++ b/app/client/src/constants/BindingsConstants.ts @@ -2,5 +2,6 @@ // TODO (hetu): Remove useless escapes and re-enable the above lint rule export type NamePathBindingMap = Record; export const DATA_BIND_REGEX = /{{(\s*[\w\.\[\]\d]+\s*)}}/g; +export const DATA_BIND_JS_REGEX = /(.*?){{(\s*(.*?)\s*)}}(.*?)/g; export const DATA_PATH_REGEX = /[\w\.\[\]\d]+/; /* eslint-enable no-useless-escape */ diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 37e80ccfa5..3e5499550a 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -121,6 +121,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { FETCH_PAGE_LIST_ERROR: "FETCH_PAGE_LIST_ERROR", FETCH_APPLICATION_LIST_ERROR: "FETCH_APPLICATION_LIST_ERROR", CREATE_APPLICATION_ERROR: "CREATE_APPLICATION_ERROR", + SAVE_JS_EXECUTION_RECORD: "SAVE_JS_EXECUTION_RECORD", }; export const ReduxFormActionTypes: { [key: string]: string } = { diff --git a/app/client/src/jsExecution/JSExecutionManagerSingleton.ts b/app/client/src/jsExecution/JSExecutionManagerSingleton.ts new file mode 100644 index 0000000000..796bb9ae03 --- /dev/null +++ b/app/client/src/jsExecution/JSExecutionManagerSingleton.ts @@ -0,0 +1,51 @@ +import RealmExecutor from "./RealmExecutor"; + +export type JSExecutorGlobal = Record; +export interface JSExecutor { + execute: (src: string, data: JSExecutorGlobal) => string; + registerLibrary: (accessor: string, lib: any) => void; + unRegisterLibrary: (accessor: string) => void; +} + +enum JSExecutorType { + REALM, +} + +class JSExecutionManager { + currentExecutor: JSExecutor; + executors: Record; + registerLibrary(accessor: string, lib: any) { + Object.keys(this.executors).forEach(type => { + const executor = this.executors[(type as any) as JSExecutorType]; + executor.registerLibrary(accessor, lib); + }); + } + unRegisterLibrary(accessor: string) { + Object.keys(this.executors).forEach(type => { + const executor = this.executors[(type as any) as JSExecutorType]; + executor.unRegisterLibrary(accessor); + }); + } + switchExecutor(type: JSExecutorType) { + const executor = this.executors[type]; + if (!executor) { + throw new Error("Executor does not exist"); + } + this.currentExecutor = executor; + } + constructor() { + const realmExecutor = new RealmExecutor(); + this.executors = { + [JSExecutorType.REALM]: realmExecutor, + }; + this.currentExecutor = realmExecutor; + + this.registerLibrary("_", window._); + } + evaluateSync(jsSrc: string, data: JSExecutorGlobal) { + return this.currentExecutor.execute(jsSrc, data); + } +} +const JSExecutionManagerSingleton = new JSExecutionManager(); + +export default JSExecutionManagerSingleton; diff --git a/app/client/src/jsExecution/RealmExecutor.ts b/app/client/src/jsExecution/RealmExecutor.ts new file mode 100644 index 0000000000..0f3c53f637 --- /dev/null +++ b/app/client/src/jsExecution/RealmExecutor.ts @@ -0,0 +1,42 @@ +import { JSExecutorGlobal, JSExecutor } from "./JSExecutionManagerSingleton"; +declare let Realm: any; + +export default class RealmExecutor implements JSExecutor { + rootRealm: any; + creaetSafeObject: any; + extrinsics: any[] = []; + createSafeFunction: (unsafeFn: Function) => Function; + + libraries: Record = {}; + constructor() { + this.rootRealm = Realm.makeRootRealm(); + this.createSafeFunction = this.rootRealm.evaluate(` + (function createSafeFunction(unsafeFn) { + return function safeFn(...args) { + unsafeFn(...args); + } + }) + `); + this.creaetSafeObject = this.rootRealm.evaluate(` + (function creaetSafeObject(unsafeObject) { + return JSON.parse(JSON.stringify(unsafeObject)); + }) + `); + } + registerLibrary(accessor: string, lib: any) { + this.rootRealm.global[accessor] = lib; + } + unRegisterLibrary(accessor: string) { + this.rootRealm.global[accessor] = null; + } + execute(sourceText: string, data: JSExecutorGlobal) { + const safeData = this.creaetSafeObject(data); + let result; + try { + result = this.rootRealm.evaluate(sourceText, safeData); + } catch (e) { + result = `Error: ${e}`; + } + return result; + } +} diff --git a/app/client/src/reducers/entityReducers/index.tsx b/app/client/src/reducers/entityReducers/index.tsx index 9e1a3a74b7..91609d1326 100644 --- a/app/client/src/reducers/entityReducers/index.tsx +++ b/app/client/src/reducers/entityReducers/index.tsx @@ -8,6 +8,7 @@ import propertyPaneConfigReducer from "./propertyPaneConfigReducer"; import datasourceReducer from "./datasourceReducer"; import bindingsReducer from "./bindingsReducer"; import pageListReducer from "./pageListReducer"; +import jsExecutionsReducer from "./jsExecutionsReducer"; const entityReducer = combineReducers({ canvasWidgets: canvasWidgetsReducer, @@ -19,6 +20,7 @@ const entityReducer = combineReducers({ datasources: datasourceReducer, nameBindings: bindingsReducer, pageList: pageListReducer, + jsExecutions: jsExecutionsReducer, }); export default entityReducer; diff --git a/app/client/src/reducers/entityReducers/jsExecutionsReducer.ts b/app/client/src/reducers/entityReducers/jsExecutionsReducer.ts new file mode 100644 index 0000000000..6ca6c85240 --- /dev/null +++ b/app/client/src/reducers/entityReducers/jsExecutionsReducer.ts @@ -0,0 +1,21 @@ +import { createReducer } from "../../utils/AppsmithUtils"; +import { + ReduxActionTypes, + ReduxAction, +} from "../../constants/ReduxActionConstants"; + +export type JSExecutionRecord = Record; +const initialState: JSExecutionRecord = {}; +const jsExecutionsReducer = createReducer(initialState, { + [ReduxActionTypes.SAVE_JS_EXECUTION_RECORD]: ( + state: JSExecutionRecord, + action: ReduxAction, + ) => { + return { + ...state, + ...action.payload, + }; + }, +}); + +export default jsExecutionsReducer; diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index fdd46b65b2..86e288286a 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -12,7 +12,11 @@ import { takeEvery, takeLatest, } from "redux-saga/effects"; -import { ActionPayload, PageAction } from "constants/ActionConstants"; +import { + ActionPayload, + PageAction, + ExecuteJSActionPayload, +} from "constants/ActionConstants"; import ActionAPI, { ActionApiResponse, ActionCreateUpdateResponse, @@ -35,6 +39,7 @@ import { extractDynamicBoundValue, getDynamicBindings, isDynamicValue, + NameBindingsWithData, } from "utils/DynamicBindingUtils"; import { validateResponse } from "./ErrorSagas"; import { getDataTree } from "selectors/entitiesSelector"; @@ -44,6 +49,8 @@ import { } from "constants/messages"; import { getFormData } from "selectors/formSelectors"; import { API_EDITOR_FORM_NAME } from "constants/forms"; +import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton"; +import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector"; export const getAction = ( state: AppState, @@ -90,6 +97,23 @@ export function* getActionParams(jsonPathKeys: string[] | undefined) { return mapToPropList(dynamicBindings); } +function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) { + const nameBindingsWithData: NameBindingsWithData = yield select( + getNameBindingsWithData, + ); + const result = JSExecutionManagerSingleton.evaluateSync( + jsAction.jsFunction, + nameBindingsWithData, + ); + + yield put({ + type: ReduxActionTypes.SAVE_JS_EXECUTION_RECORD, + payload: { + [jsAction.jsFunctionId]: result, + }, + }); +} + export function* executeAPIQueryActionSaga(apiAction: ActionPayload) { try { const api: PageAction = yield select(getAction, apiAction.actionId); @@ -193,6 +217,11 @@ export function* executeActionSaga(actionPayloads: ActionPayload[]): any { return call(executeAPIQueryActionSaga, actionPayload); case "QUERY": return call(executeAPIQueryActionSaga, actionPayload); + case "JS_FUNCTION": + return call( + executeJSActionSaga, + actionPayload as ExecuteJSActionPayload, + ); default: return undefined; } diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 69fae442c6..32d2fc14e9 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -7,7 +7,10 @@ import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigRe import { WidgetCardProps } from "widgets/BaseWidget"; import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; -import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; +import { + enhanceWithDynamicValuesAndValidations, + NameBindingsWithData, +} from "utils/DynamicBindingUtils"; import { getDataTree } from "./entitiesSelector"; import { FlattenedWidgetProps, @@ -17,6 +20,7 @@ import { PageListReduxState } from "reducers/entityReducers/pageListReducer"; import { OccupiedSpace } from "constants/editorConstants"; import { WidgetTypes } from "constants/WidgetConstants"; +import { getNameBindingsWithData } from "./nameBindingsWithDataSelector"; const getEditorState = (state: AppState) => state.ui.editor; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; @@ -112,12 +116,13 @@ export const getWidgetCards = createSelector( export const getValidatedDynamicProps = createSelector( getDataTree, - (entities: DataTree) => { + getNameBindingsWithData, + (entities: DataTree, nameBindingsWithData: NameBindingsWithData) => { const widgets = { ...entities.canvasWidgets }; Object.keys(widgets).forEach(widgetKey => { widgets[widgetKey] = enhanceWithDynamicValuesAndValidations( widgets[widgetKey], - entities, + nameBindingsWithData, true, ); }); diff --git a/app/client/src/selectors/nameBindingsWithDataSelector.ts b/app/client/src/selectors/nameBindingsWithDataSelector.ts new file mode 100644 index 0000000000..55ae87f2d7 --- /dev/null +++ b/app/client/src/selectors/nameBindingsWithDataSelector.ts @@ -0,0 +1,24 @@ +import { DataTree } from "reducers"; +import { NameBindingsWithData } from "utils/DynamicBindingUtils"; +import { JSONPath } from "jsonpath-plus"; +import { createSelector } from "reselect"; +import { getDataTree } from "./entitiesSelector"; + +export const getNameBindingsWithData = createSelector( + getDataTree, + (dataTree: DataTree): NameBindingsWithData => { + const nameBindingsWithData: Record = {}; + Object.keys(dataTree.nameBindings).forEach(key => { + const nameBindings = dataTree.nameBindings[key]; + const evaluatedValue = JSONPath({ + path: nameBindings, + json: dataTree, + })[0]; + if (evaluatedValue && key !== "undefined") { + nameBindingsWithData[key] = evaluatedValue; + } + }); + + return nameBindingsWithData; + }, +); diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index 300d4f1a89..9dc1489779 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -1,12 +1,15 @@ import { createSelector } from "reselect"; -import { AppState, DataTree } from "reducers"; +import { AppState } from "reducers"; import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer"; import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer"; -import { getDataTree } from "./entitiesSelector"; -import { enhanceWithDynamicValuesAndValidations } from "utils/DynamicBindingUtils"; +import { + enhanceWithDynamicValuesAndValidations, + NameBindingsWithData, +} from "utils/DynamicBindingUtils"; import { WidgetProps } from "widgets/BaseWidget"; +import { getNameBindingsWithData } from "./nameBindingsWithDataSelector"; const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => state.ui.propertyPane; @@ -35,10 +38,17 @@ export const getCurrentWidgetProperties = createSelector( export const getWidgetPropsWithValidations = createSelector( getCurrentWidgetProperties, - getDataTree, - (widget: WidgetProps | undefined, dataTree: DataTree) => { + getNameBindingsWithData, + ( + widget: WidgetProps | undefined, + nameBindigsWithData: NameBindingsWithData, + ) => { if (!widget) return undefined; - return enhanceWithDynamicValuesAndValidations(widget, dataTree, false); + return enhanceWithDynamicValuesAndValidations( + widget, + nameBindigsWithData, + false, + ); }, ); diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index a51797e43d..a1022f6eae 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -1,22 +1,67 @@ import _ from "lodash"; -import { DataTree } from "reducers"; -import { JSONPath } from "jsonpath-plus"; import { WidgetProps } from "widgets/BaseWidget"; -import { DATA_BIND_REGEX, DATA_PATH_REGEX } from "constants/BindingsConstants"; +import { DATA_BIND_JS_REGEX } from "constants/BindingsConstants"; import ValidationFactory from "./ValidationFactory"; +import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton"; +export type NameBindingsWithData = Record; export const isDynamicValue = (value: string): boolean => - DATA_BIND_REGEX.test(value); + DATA_BIND_JS_REGEX.test(value); + +//{{}}{{}}} +function parseDynamicString(dynamicString: string): string[] { + let parsedDynamicValues = []; + const indexOfDoubleParanStart = dynamicString.indexOf("{{"); + if (indexOfDoubleParanStart === -1) { + return [dynamicString]; + } + //{{}}{{}}} + const firstString = dynamicString.substring(0, indexOfDoubleParanStart); + firstString && parsedDynamicValues.push(firstString); + let rest = dynamicString.substring( + indexOfDoubleParanStart, + dynamicString.length, + ); + //{{}}{{}}} + let sum = 0; + for (let i = 0; i <= rest.length - 1; i++) { + const char = rest[i]; + const prevChar = rest[i - 1]; + + if (char === "{") { + sum++; + } else if (char === "}") { + sum--; + if (prevChar === "}" && sum === 0) { + parsedDynamicValues.push(rest.substring(0, i + 1)); + rest = rest.substring(i + 1, rest.length); + if (rest) { + parsedDynamicValues = parsedDynamicValues.concat( + parseDynamicString(rest), + ); + break; + } + } + } + } + if (sum !== 0 && dynamicString !== "") { + return [dynamicString]; + } + return parsedDynamicValues; +} export const getDynamicBindings = ( dynamicString: string, ): { bindings: string[]; paths: string[] } => { // Get the {{binding}} bound values - const bindings = dynamicString.match(DATA_BIND_REGEX) || []; + const bindings = parseDynamicString(dynamicString); // Get the "binding" path values - const paths = bindings.map(p => { - const matches = p.match(DATA_PATH_REGEX); - if (matches) return matches[0]; + const paths = bindings.map(binding => { + const length = binding.length; + const matches = binding.match(DATA_BIND_JS_REGEX); + if (matches) { + return binding.substring(2, length - 2); + } return ""; }); return { bindings, paths }; @@ -24,17 +69,10 @@ export const getDynamicBindings = ( // Paths are expected to have "{name}.{path}" signature export const extractDynamicBoundValue = ( - dataTree: DataTree, + data: NameBindingsWithData, path: string, ): any => { - // Remove the name in the binding - const splitPath = path.split("."); - // Find the dataTree path of the name - const bindingPath = dataTree.nameBindings[splitPath[0]]; - // Create the full path - const fullPath = `${bindingPath}.${splitPath.slice(1).join(".")}`; - // Search with JSONPath - return JSONPath({ path: fullPath, json: dataTree })[0]; + return JSExecutionManagerSingleton.evaluateSync(path, data); }; // For creating a final value where bindings could be in a template format @@ -57,13 +95,16 @@ export const createDynamicValueString = ( export const getDynamicValue = ( dynamicBinding: string, - dataTree: DataTree, + data: NameBindingsWithData, ): any => { // Get the {{binding}} bound values const { bindings, paths } = getDynamicBindings(dynamicBinding); if (bindings.length) { // Get the Data Tree value of those "binding "paths - const values = paths.map(p => extractDynamicBoundValue(dataTree, p)); + const values = paths.map((p, i) => { + return p ? extractDynamicBoundValue(data, p) : bindings[i]; + }); + // if it is just one binding, no need to create template string if (bindings.length === 1) return values[0]; // else return a string template with bindings @@ -74,17 +115,19 @@ export const getDynamicValue = ( export const enhanceWithDynamicValuesAndValidations = ( widget: WidgetProps, - entities: DataTree, + nameBindingsWithData: NameBindingsWithData, replaceWithParsed: boolean, ): WidgetProps => { if (!widget) return widget; const properties = { ...widget }; const invalidProps: Record = {}; + const t0 = performance.now(); + Object.keys(widget).forEach((property: string) => { let value = widget[property]; // Check for dynamic bindings if (widget.dynamicBindings && property in widget.dynamicBindings) { - value = getDynamicValue(value, entities); + value = getDynamicValue(value, nameBindingsWithData); } // Pass it through validation and parse const { isValid, parsed } = ValidationFactory.validateWidgetProperty( @@ -97,5 +140,14 @@ export const enhanceWithDynamicValuesAndValidations = ( // Replace if flag is turned on if (replaceWithParsed) properties[property] = parsed; }); + const t1 = performance.now(); + console.log( + "Evaluations for " + + widget.widgetName + + " took " + + (t1 - t0) + + " milliseconds.", + ); + console.trace(); return { ...properties, invalidProps }; }; diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index f75fbe2816..5a4b0e52ab 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -2,7 +2,7 @@ import React from "react"; import _ from "lodash"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; -import { ActionPayload } from "constants/ActionConstants"; +import { ActionPayload, BaseActionPayload } from "constants/ActionConstants"; import { AutoResizer } from "react-base-table"; import "react-base-table/styles.css"; import { forIn } from "lodash"; @@ -94,7 +94,7 @@ class TableWidget extends BaseWidget { export type PaginationType = "PAGES" | "INFINITE_SCROLL"; -export interface TableAction extends ActionPayload { +export interface TableAction extends BaseActionPayload { actionName: string; } diff --git a/app/client/typings/Realm/index.d.ts b/app/client/typings/Realm/index.d.ts new file mode 100644 index 0000000000..ea78bb32e9 --- /dev/null +++ b/app/client/typings/Realm/index.d.ts @@ -0,0 +1,2 @@ +// import * as React from "react"; +declare module "Realm";