diff --git a/app/client/.eslintrc.js b/app/client/.eslintrc.js index 386c8ee8f04..aa0dd5f9eca 100644 --- a/app/client/.eslintrc.js +++ b/app/client/.eslintrc.js @@ -1,18 +1,18 @@ module.exports = { - parser: '@typescript-eslint/parser', + parser: "@typescript-eslint/parser", plugins: ["react", "@typescript-eslint", "prettier", "react-hooks"], extends: [ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react "plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint", - "plugin:prettier/recommended" + "plugin:prettier/recommended", ], parserOptions: { ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features sourceType: "module", // Allows for the use of imports ecmaFeatures: { - jsx: true // Allows for the parsing of JSX - } + jsx: true, // Allows for the parsing of JSX + }, }, rules: { "@typescript-eslint/explicit-function-return-type": 0, @@ -20,11 +20,12 @@ module.exports = { "react-hooks/rules-of-hooks": "error", "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-var-requires": 0, + "import/no-webpack-loader-syntax": 0 }, settings: { react: { pragma: "React", - version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use - } - } + version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use + }, + }, }; diff --git a/app/client/build.sh b/app/client/build.sh index f2e36f7871f..253ec39cbe4 100755 --- a/app/client/build.sh +++ b/app/client/build.sh @@ -2,7 +2,7 @@ GIT_SHA=$(eval git rev-parse HEAD) echo $GIT_SHA -REACT_APP_SENTRY_RELEASE=$GIT_SHA craco --max-old-space-size=4096 build --config craco.build.config.js +REACT_APP_SENTRY_RELEASE=$GIT_SHA EXTEND_ESLINT=true craco --max-old-space-size=4096 build --config craco.build.config.js rm ./build/static/js/*.js.map -echo "build finished" \ No newline at end of file +echo "build finished" diff --git a/app/client/cypress.json b/app/client/cypress.json index 0cee6e22848..37f524063b7 100644 --- a/app/client/cypress.json +++ b/app/client/cypress.json @@ -12,5 +12,9 @@ "json": false }, "viewportHeight": 900, - "viewportWidth": 1400 -} \ No newline at end of file + "viewportWidth": 1400, + "retries": { + "runMode": 2, + "openMode": 0 + } +} diff --git a/app/client/cypress/fixtures/SimpleBinding.json b/app/client/cypress/fixtures/SimpleBinding.json new file mode 100644 index 00000000000..b9dad941ce1 --- /dev/null +++ b/app/client/cypress/fixtures/SimpleBinding.json @@ -0,0 +1,62 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "dynamicBindings": {}, + "version": 6, + "minHeight": 1292, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "text": "Label", + "textStyle": "LABEL", + "textAlign": "LEFT", + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 3, + "rightColumn": 7, + "topRow": 4, + "bottomRow": 5, + "parentId": "0", + "widgetId": "pcznwg0g8k" + }, + { + "isVisible": true, + "text": "{{Text1.text}}", + "textStyle": "LABEL", + "textAlign": "LEFT", + "widgetName": "Text2", + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 3, + "rightColumn": 7, + "topRow": 6, + "bottomRow": 7, + "parentId": "0", + "widgetId": "tgnz7xg7a3", + "dynamicBindings": { + "text": true + } + } + ] + }, + "layoutOnLoadActions": [] +} diff --git a/app/client/cypress/fixtures/formWidgetdsl.json b/app/client/cypress/fixtures/formWidgetdsl.json index a06bc610cad..c0a833484fc 100644 --- a/app/client/cypress/fixtures/formWidgetdsl.json +++ b/app/client/cypress/fixtures/formWidgetdsl.json @@ -81,59 +81,6 @@ "widgetId": "xlrmeiioaa" } ], - "blueprint": { - "view": [ - { - "type": "TEXT_WIDGET", - "size": { - "rows": 1, - "cols": 12 - }, - "position": { - "top": 0, - "left": 0 - }, - "props": { - "text": "Form", - "textStyle": "HEADING" - } - }, - { - "type": "FORM_BUTTON_WIDGET", - "size": { - "rows": 1, - "cols": 4 - }, - "position": { - "top": 11, - "left": 12 - }, - "props": { - "text": "Submit", - "buttonStyle": "PRIMARY_BUTTON", - "disabledWhenInvalid": true, - "resetFormOnClick": true - } - }, - { - "type": "FORM_BUTTON_WIDGET", - "size": { - "rows": 1, - "cols": 4 - }, - "position": { - "top": 11, - "left": 8 - }, - "props": { - "text": "Reset", - "buttonStyle": "SECONDARY_BUTTON", - "disabledWhenInvalid": false, - "resetFormOnClick": true - } - } - ] - }, "minHeight": 520, "type": "CANVAS_WIDGET", "isLoading": false, @@ -147,76 +94,6 @@ "widgetId": "sidaue1kdu" } ], - "blueprint": { - "view": [ - { - "type": "CANVAS_WIDGET", - "position": { - "top": 0, - "left": 0 - }, - "props": { - "containerStyle": "none", - "canExtend": false, - "detachFromLayout": true, - "children": [], - "blueprint": { - "view": [ - { - "type": "TEXT_WIDGET", - "size": { - "rows": 1, - "cols": 12 - }, - "position": { - "top": 0, - "left": 0 - }, - "props": { - "text": "Form", - "textStyle": "HEADING" - } - }, - { - "type": "FORM_BUTTON_WIDGET", - "size": { - "rows": 1, - "cols": 4 - }, - "position": { - "top": 11, - "left": 12 - }, - "props": { - "text": "Submit", - "buttonStyle": "PRIMARY_BUTTON", - "disabledWhenInvalid": true, - "resetFormOnClick": true - } - }, - { - "type": "FORM_BUTTON_WIDGET", - "size": { - "rows": 1, - "cols": 4 - }, - "position": { - "top": 11, - "left": 8 - }, - "props": { - "text": "Reset", - "buttonStyle": "SECONDARY_BUTTON", - "disabledWhenInvalid": false, - "resetFormOnClick": true - } - } - ] - } - } - } - ] - }, "type": "FORM_WIDGET", "isLoading": false, "parentColumnSpace": 74, @@ -230,4 +107,4 @@ } ] } -} \ No newline at end of file +} diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Entity_delete_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Entity_delete_spec.js new file mode 100644 index 00000000000..2d14e84a432 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Entity_delete_spec.js @@ -0,0 +1,19 @@ +const dsl = require("../../../fixtures/SimpleBinding.json"); +const widgetsPage = require("../../../locators/Widgets.json"); + +describe("Binding the multiple widgets and validating default data", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Checks if delete will remove bindings", function() { + cy.get(widgetsPage.textWidget) + .first() + .click({ force: true }); + cy.get("body").type("{del}", { force: true }); + + cy.get(widgetsPage.textWidget) + .first() + .should("not.have.text", "Label"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/InputWidgets_NavigateTo_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/InputWidgets_NavigateTo_validation_spec.js index a8d4a158308..6298c8b0d7a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/InputWidgets_NavigateTo_validation_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/InputWidgets_NavigateTo_validation_spec.js @@ -49,6 +49,7 @@ describe("Binding the multiple Widgets and validating NavigateTo Page", function cy.get(publish.inputGrp) .first() .type("123"); + cy.get(widgetsPage.chartWidget).should("be.visible"); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormReset_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormReset_spec.js index 7e3290ce815..631af0d6330 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormReset_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormReset_spec.js @@ -20,6 +20,8 @@ describe("Form reset functionality", function() { .contains("Reset") .click(); + cy.wait(500); + cy.get(".tr") .eq(2) .should("not.have.class", "selected-row"); diff --git a/app/client/cypress/locators/ViewWidgets.json b/app/client/cypress/locators/ViewWidgets.json index 2c83bf74648..b9606194a0d 100644 --- a/app/client/cypress/locators/ViewWidgets.json +++ b/app/client/cypress/locators/ViewWidgets.json @@ -14,7 +14,7 @@ "searchloc": "input[placeholder='Enter location to search']", "imagecontainer": ".t--draggable-imagewidget span.t--widget-name", "imageinner": ".t--draggable-imagewidget img", - "chartInnerText": ".t--draggable-chartwidget text", + "chartInnerText": ".t--property-control-title", "inputChartValue": ".t--property-control-chartdata .CodeMirror textarea", "chartButton": ".t--property-control-chartdata button", "rectangleChart": ".t--draggable-chartwidget g rect", diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 82c3cab0e0e..a747790c176 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -882,8 +882,8 @@ Cypress.Commands.add( Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { cy.get(commonlocators.editWidgetName) - .dblclick({ force: true }) - .type(text, { force: true }) + .click({ force: true }) + .type(text) .type("{enter}"); cy.get(inputcss) .first() @@ -1174,23 +1174,7 @@ Cypress.Commands.add("getAlert", alertcss => { .contains("Success") .click({ force: true }); }); -Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { - cy.get(commonlocators.editWidgetName) - .dblclick({ force: true }) - .type(text) - .type("{enter}"); - cy.get(inputcss) - .first() - .trigger("mouseover", { force: true }); - cy.get(innercss).should("have.text", text); -}); -Cypress.Commands.add("radioInput", (index, text) => { - cy.get(widgetsPage.RadioInput) - .eq(index) - .click() - .clear() - .type(text); -}); + Cypress.Commands.add("tabVerify", (index, text) => { cy.get(".t--property-control-tabs input") .eq(index) @@ -1213,26 +1197,6 @@ Cypress.Commands.add("togglebarDisable", value => { .uncheck({ force: true }) .should("not.checked"); }); -Cypress.Commands.add("radiovalue", (value, value2) => { - cy.get(value) - .click() - .clear() - .type(value2); -}); -Cypress.Commands.add("optionValue", (value, value2) => { - cy.get(value) - .click() - .clear() - .type(value2); -}); -Cypress.Commands.add("dropdownDynamic", text => { - cy.wait(2000); - cy.get("ul[class='bp3-menu']") - .first() - .contains(text) - .click({ force: true }) - .should("have.text", text); -}); Cypress.Commands.add("getAlert", alertcss => { cy.get(commonlocators.dropdownSelectButton).click({ force: true }); @@ -1251,16 +1215,7 @@ Cypress.Commands.add("getAlert", alertcss => { .contains("Success") .click({ force: true }); }); -Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { - cy.get(commonlocators.editWidgetName) - .dblclick({ force: true }) - .type(text) - .type("{enter}"); - cy.get(inputcss) - .first() - .trigger("mouseover", { force: true }); - cy.get(innercss).should("have.text", text); -}); + Cypress.Commands.add("radioInput", (index, text) => { cy.get(widgetsPage.RadioInput) .eq(index) @@ -1648,8 +1603,6 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("DELETE", "/api/v1/applications/*").as("deleteApp"); cy.route("DELETE", "/api/v1/actions/*").as("deleteAction"); cy.route("DELETE", "/api/v1/pages/*").as("deletePage"); - - cy.route("GET", "/api/v1/plugins/*/form").as("getPluginForm"); cy.route("POST", "/api/v1/datasources").as("createDatasource"); cy.route("POST", "/api/v1/datasources/test").as("testDatasource"); cy.route("PUT", "/api/v1/datasources/*").as("saveDatasource"); diff --git a/app/client/package.json b/app/client/package.json index 186a163c43c..ae1cb29ab76 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -117,14 +117,14 @@ "typescript": "^3.9.2", "unescape-js": "^1.1.4", "url-search-params-polyfill": "^8.0.0", - "workerize-loader": "^1.2.0" + "worker-loader": "^3.0.2" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", - "start": "REACT_APP_BASE_URL=https://release-api.appsmith.com REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start", + "start": "EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start", "build": "./build.sh", "build-local": "craco --max-old-space-size=4096 build --config craco.build.config.js", - "build-staging": " REACT_APP_ENVIRONMENT=STAGING craco --max-old-space-size=4096 build --config craco.build.config.js", + "build-staging": "REACT_APP_ENVIRONMENT=STAGING craco --max-old-space-size=4096 build --config craco.build.config.js", "test": "CYPRESS_BASE_URL=https://dev.appsmith.com cypress/test.sh", "test:ci": "CYPRESS_BASE_URL=https://dev.appsmith.com cypress/test.sh --env=ci", "eject": "react-scripts eject", @@ -209,4 +209,4 @@ "pre-commit": "lint-staged" } } -} \ No newline at end of file +} diff --git a/app/client/public/index.html b/app/client/public/index.html index b7befa3bd50..5178386def4 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -140,7 +140,6 @@ }; - diff --git a/app/client/public/shims/realms-shim.umd.min.js b/app/client/public/shims/realms-shim.umd.min.js deleted file mode 100644 index 2b74c0b9ccf..00000000000 --- a/app/client/public/shims/realms-shim.umd.min.js +++ /dev/null @@ -1,10 +0,0 @@ -(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/actions/batchActions.ts b/app/client/src/actions/batchActions.ts index d8367f25858..76c42d6755a 100644 --- a/app/client/src/actions/batchActions.ts +++ b/app/client/src/actions/batchActions.ts @@ -6,3 +6,8 @@ export const batchAction = (action: ReduxAction) => ({ }); export type BatchAction = ReduxAction>; + +export const batchActionSuccess = (actions: ReduxAction[]) => ({ + type: ReduxActionTypes.BATCH_UPDATES_SUCCESS, + payload: actions, +}); diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index 7102ce3cd6e..465be957581 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -32,6 +32,14 @@ export const fetchPage = (pageId: string): ReduxAction => { }; }; +export const fetchPublishedPage = (pageId: string, bustCache = false) => ({ + type: ReduxActionTypes.FETCH_PUBLISHED_PAGE_INIT, + payload: { + pageId, + bustCache, + }, +}); + export const fetchPageSuccess = () => { return { type: ReduxActionTypes.FETCH_PAGE_SUCCESS, diff --git a/app/client/src/api/Api.tsx b/app/client/src/api/Api.tsx index f99b6721250..894461bdcf2 100644 --- a/app/client/src/api/Api.tsx +++ b/app/client/src/api/Api.tsx @@ -1,4 +1,3 @@ -import _ from "lodash"; import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; import { REQUEST_TIMEOUT_MS, diff --git a/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx b/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx index 273a97dc6dc..3168dd0efd0 100644 --- a/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx +++ b/app/client/src/components/designSystems/appsmith/help/DocumentationSearch.tsx @@ -104,7 +104,7 @@ const Hit = (props: { hit: { path: string } }) => { const DefaultHelpMenuItem = (props: { item: { label: string; link?: string; id?: string; icon: React.ReactNode }; - onSelect: Function; + onSelect: () => void; }) => { return (
  • diff --git a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.test.ts b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.test.ts index 54e36144331..2e7916175fa 100644 --- a/app/client/src/components/editorComponents/CodeEditor/hintHelpers.test.ts +++ b/app/client/src/components/editorComponents/CodeEditor/hintHelpers.test.ts @@ -1,14 +1,7 @@ import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers"; import { MockCodemirrorEditor } from "../../../../test/__mocks__/CodeMirrorEditorMock"; -import RealmExecutor from "jsExecution/RealmExecutor"; -jest.mock("jsExecution/RealmExecutor"); describe("hint helpers", () => { - beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore - // @ts-ignore - RealmExecutor.mockClear(); - }); describe("binding hint helper", () => { it("is initialized correctly", () => { // eslint-disable-next-line @typescript-eslint/ban-ts-ignore diff --git a/app/client/src/components/editorComponents/ToastComponent.tsx b/app/client/src/components/editorComponents/ToastComponent.tsx index 1b56650eafe..1a06954b2e6 100644 --- a/app/client/src/components/editorComponents/ToastComponent.tsx +++ b/app/client/src/components/editorComponents/ToastComponent.tsx @@ -83,6 +83,10 @@ const ToastComponent = (props: Props) => { const Toaster = { show: (config: Props) => { + if (typeof config.message !== "string") { + console.error("Toast message needs to be a string"); + return; + } toast( = { payload: T; }; -type ActionDispatcher = ( +export type ActionDispatcher = ( ...args: A ) => ActionDescription; @@ -76,60 +76,39 @@ type DataTreeSeed = { }; export class DataTreeFactory { - static create( - { actions, widgets, widgetsMeta, pageList, appData }: DataTreeSeed, - // TODO(hetu) - // temporary fix for not getting functions while normal evals which crashes the app - // need to remove this after we get a proper solve - withFunctions?: boolean, - ): DataTree { + static create({ + actions, + widgets, + widgetsMeta, + pageList, + appData, + }: DataTreeSeed): DataTree { const dataTree: DataTree = {}; - const actionPaths = []; - actions.forEach(a => { - const config = a.config; + actions.forEach(action => { let dynamicBindingPathList: Property[] = []; // update paths if ( - config.dynamicBindingPathList && - config.dynamicBindingPathList.length + action.config.dynamicBindingPathList && + action.config.dynamicBindingPathList.length ) { - dynamicBindingPathList = config.dynamicBindingPathList.map(d => ({ - ...d, - key: `config.${d.key}`, - })); + dynamicBindingPathList = action.config.dynamicBindingPathList.map( + d => ({ + ...d, + key: `config.${d.key}`, + }), + ); } - dataTree[config.name] = { - ...a, - actionId: config.id, - name: config.name, - pluginType: config.pluginType, - config: config.actionConfiguration, + dataTree[action.config.name] = { + run: {}, + actionId: action.config.id, + name: action.config.name, + pluginType: action.config.pluginType, + config: action.config.actionConfiguration, dynamicBindingPathList, - data: a.data ? a.data.body : {}, - run: withFunctions - ? function( - this: DataTreeAction, - onSuccess: string, - onError: string, - params = "", - ) { - return { - type: "RUN_ACTION", - payload: { - actionId: this.actionId, - onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "", - onError: onError ? `{{${onError.toString()}}}` : "", - params, - }, - }; - } - : {}, + data: action.data ? action.data.body : {}, ENTITY_TYPE: ENTITY_TYPE.ACTION, + isLoading: action.isLoading, }; - if (withFunctions) { - actionPaths.push(`${config.name}.run`); - } - dataTree.actionPaths && dataTree.actionPaths.push(); }); Object.keys(widgets).forEach(w => { const widget = { ...widgets[w] }; @@ -165,63 +144,7 @@ export class DataTreeFactory { }; }); - if (withFunctions) { - dataTree.navigateTo = function( - pageNameOrUrl: string, - params: Record, - ) { - return { - type: "NAVIGATE_TO", - payload: { pageNameOrUrl, params }, - }; - }; - actionPaths.push("navigateTo"); - - dataTree.showAlert = function(message: string, style: string) { - return { - type: "SHOW_ALERT", - payload: { message, style }, - }; - }; - actionPaths.push("showAlert"); - - // dataTree.url = url; - dataTree.showModal = function(modalName: string) { - return { - type: "SHOW_MODAL_BY_NAME", - payload: { modalName }, - }; - }; - actionPaths.push("showModal"); - - dataTree.closeModal = function(modalName: string) { - return { - type: "CLOSE_MODAL", - payload: { modalName }, - }; - }; - actionPaths.push("closeModal"); - - dataTree.storeValue = function(key: string, value: string) { - return { - type: "STORE_VALUE", - payload: { key, value }, - }; - }; - actionPaths.push("storeValue"); - - dataTree.download = function(data: string, name: string, type: string) { - return { - type: "DOWNLOAD", - payload: { data, name, type }, - }; - }; - actionPaths.push("download"); - } - dataTree.pageList = pageList; - dataTree.actionPaths = actionPaths; - dataTree.appsmith = { ...appData } as DataTreeAppsmith; (dataTree.appsmith as DataTreeAppsmith).ENTITY_TYPE = ENTITY_TYPE.APPSMITH; return dataTree; diff --git a/app/client/src/jsExecution/JSExecutionManagerSingleton.ts b/app/client/src/jsExecution/JSExecutionManagerSingleton.ts deleted file mode 100644 index e07668fb486..00000000000 --- a/app/client/src/jsExecution/JSExecutionManagerSingleton.ts +++ /dev/null @@ -1,107 +0,0 @@ -import RealmExecutor from "./RealmExecutor"; -import moment from "moment-timezone"; -import { ActionDescription } from "entities/DataTree/dataTreeFactory"; -import { btoa, atob, version as BASE64LIBVERSION } from "js-base64"; -import { VERSION as lodashVersion } from "lodash"; -export type JSExecutorGlobal = Record; -export type JSExecutorResult = { - result: any; - triggers?: ActionDescription[]; -}; -export interface JSExecutor { - execute: ( - src: string, - data: JSExecutorGlobal, - callbackData?: any, - ) => JSExecutorResult; - registerLibrary: (accessor: string, lib: any) => void; - unRegisterLibrary: (accessor: string) => void; -} - -enum JSExecutorType { - REALM, -} - -export type ExtraLibrary = { - version: string; - docsURL: string; - displayName: string; - accessor: string; - lib: any; -}; - -export const extraLibraries: ExtraLibrary[] = [ - { - accessor: "_", - lib: window._, - version: lodashVersion, - docsURL: `https://lodash.com/docs/${lodashVersion}`, - displayName: "lodash", - }, - { - accessor: "moment", - lib: moment, - version: moment.version, - docsURL: `https://momentjs.com/docs/`, - displayName: "moment", - }, - { - accessor: "btoa", - lib: btoa, - version: BASE64LIBVERSION, - docsURL: "https://github.com/dankogai/js-base64#readme", - displayName: "btoa", - }, - { - accessor: "atob", - lib: atob, - version: BASE64LIBVERSION, - docsURL: "https://github.com/dankogai/js-base64#readme", - displayName: "atob", - }, -]; - -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; - - extraLibraries.forEach(config => { - this.registerLibrary(config.accessor, config.lib); - }); - } - evaluateSync( - jsSrc: string, - data: JSExecutorGlobal, - callbackData?: any, - ): JSExecutorResult { - return this.currentExecutor.execute(jsSrc, data, callbackData); - } -} -const JSExecutionManagerSingleton = new JSExecutionManager(); - -export default JSExecutionManagerSingleton; diff --git a/app/client/src/jsExecution/RealmExecutor.ts b/app/client/src/jsExecution/RealmExecutor.ts deleted file mode 100644 index 164a5221224..00000000000 --- a/app/client/src/jsExecution/RealmExecutor.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - JSExecutorGlobal, - JSExecutor, - JSExecutorResult, -} from "./JSExecutionManagerSingleton"; -import JSONFn from "json-fn"; -import log from "loglevel"; -declare let Realm: any; - -export default class RealmExecutor implements JSExecutor { - rootRealm: any; - createSafeObject: any; - extrinsics: any[] = []; - createSafeFunction: (unsafeFn: Function) => Function; - - libraries: Record = {}; - constructor() { - this.rootRealm = Realm.makeRootRealm(); - this.registerLibrary("JSONFn", JSONFn); - this.createSafeFunction = this.rootRealm.evaluate(` - (function createSafeFunction(unsafeFn) { - return function safeFn(...args) { - return unsafeFn(...args); - } - }) - `); - // After parsing the data we add a triggers list on the global scope to - // push to it during any script execution - // We replace all action descriptor functions with our pusher function - // which has reference to the triggers via binding - this.createSafeObject = this.rootRealm.evaluate( - ` - (function createSafeObject(unsafeObject) { - const safeObject = JSONFn.parse(JSONFn.stringify(unsafeObject)); - if(safeObject.actionPaths) { - safeObject.triggers = []; - const pusher = function (action, ...payload) { - const actionPayload = action(...payload); - this.triggers.push(actionPayload); - } - safeObject.actionPaths.forEach(path => { - const action = _.get(safeObject, path); - const entity = _.get(safeObject, path.split(".")[0]) - if(action) { - _.set(safeObject, path, pusher.bind(safeObject, action.bind(entity))) - } - }) - } - return safeObject - }) - `, - ); - } - - registerLibrary(accessor: string, lib: any) { - this.rootRealm.global[accessor] = lib; - } - unRegisterLibrary(accessor: string) { - this.rootRealm.global[accessor] = null; - } - private convertToMainScope(result: any) { - if (typeof result === "object") { - if (Array.isArray(result)) { - return Object.assign([], result); - } - return Object.assign({}, result); - } - return result; - } - execute( - sourceText: string, - data: JSExecutorGlobal, - callbackData?: any, - ): JSExecutorResult { - const safeCallbackData = this.createSafeObject(callbackData || {}); - const safeData = this.createSafeObject(data); - try { - // We create a closed function and evaluate that - // This is to send any triggers received during evaluations - // triggers should already be defined in the safeData - const scriptToEvaluate = ` - function closedFunction () { - const result = ${sourceText}; - return { result, triggers } - } - closedFunction() - `; - - const scriptWithCallback = ` - function callback (script) { - const userFunction = script; - const result = userFunction(CALLBACK_DATA); - return { result, triggers }; - } - callback(${sourceText}); - `; - const script = callbackData ? scriptWithCallback : scriptToEvaluate; - const data = callbackData - ? { ...safeData, CALLBACK_DATA: safeCallbackData } - : safeData; - const { result, triggers } = this.rootRealm.evaluate(script, data); - return { - result: this.convertToMainScope(result), - triggers, - }; - } catch (e) { - log.debug(`Error: "${e.message}" when evaluating {{${sourceText}}}`); - log.debug(e); - return { result: undefined, triggers: [] }; - } - } -} diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 48fd6f2e432..4e8a028792f 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -434,6 +434,12 @@ const WidgetConfigResponse: WidgetConfigReducerState = { mapCenter: { lat: -34.397, long: 150.644 }, defaultMarkers: [{ lat: -34.397, long: 150.644, title: "Test A" }], }, + SKELETON_WIDGET: { + isLoading: true, + rows: 1, + columns: 1, + widgetName: "Skeleton", + }, }, configVersion: 1, }; diff --git a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx index f7df9a1ca9c..31080a185bb 100644 --- a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx +++ b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx @@ -1,7 +1,6 @@ import React, { Component } from "react"; import { RouteComponentProps, Link } from "react-router-dom"; import { connect } from "react-redux"; -import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { getIsFetchingPage } from "selectors/appViewSelectors"; import styled from "styled-components"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; @@ -18,6 +17,11 @@ import { } from "selectors/editorSelectors"; import ConfirmRunModal from "pages/Editor/ConfirmRunModal"; import { getCurrentApplication } from "selectors/applicationSelectors"; +import { + isPermitted, + PERMISSION_TYPE, +} from "../Applications/permissionHelpers"; +import { fetchPublishedPage } from "actions/pageActions"; const Section = styled.section` background: ${props => props.theme.colors.bodyBG}; @@ -33,15 +37,10 @@ type AppViewerPageContainerProps = { currentPageName?: string; currentAppName?: string; fetchPage: (pageId: string, bustCache?: boolean) => void; + currentAppPermissions?: string[]; } & RouteComponentProps; class AppViewerPageContainer extends Component { - componentDidMount() { - const { pageId } = this.props.match.params; - if (pageId) { - this.props.fetchPage(pageId, true); - } - } componentDidUpdate(previously: AppViewerPageContainerProps) { const { pageId } = this.props.match.params; if ( @@ -52,6 +51,28 @@ class AppViewerPageContainer extends Component { } } render() { + let appsmithEditorLink; + if ( + this.props.currentAppPermissions && + isPermitted( + this.props.currentAppPermissions, + PERMISSION_TYPE.MANAGE_APPLICATION, + ) + ) { + appsmithEditorLink = ( +

    + Please add widgets to this page in the  + + Appsmith Editor + +

    + ); + } const pageNotFound = ( { /> } title="This page seems to be blank" - description={ -

    - Please add widgets to this page in the  - - Appsmith Editor - -

    - } + description={appsmithEditorLink} />
    ); @@ -118,19 +127,14 @@ const mapStateToProps = (state: AppState) => { widgets: getCanvasWidgetDsl(state), currentPageName: getCurrentPageName(state), currentAppName: currentApp?.name, + currentAppPermissions: currentApp?.userPermissions, }; return props; }; const mapDispatchToProps = (dispatch: any) => ({ fetchPage: (pageId: string, bustCache = false) => - dispatch({ - type: ReduxActionTypes.FETCH_PUBLISHED_PAGE_INIT, - payload: { - pageId, - bustCache, - }, - }), + dispatch(fetchPublishedPage(pageId, bustCache)), }); export default connect( diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx index e02e8a199bb..39c4e3cf499 100644 --- a/app/client/src/pages/Applications/index.tsx +++ b/app/client/src/pages/Applications/index.tsx @@ -80,9 +80,69 @@ const OrgSection = styled.div``; const PaddingWrapper = styled.div` width: ${props => props.theme.card.minWidth + props.theme.spaces[5] * 2}px; - margin: ${props => props.theme.spaces[6] + 1}px - ${props => props.theme.spaces[12] + 2}px + margin: ${props => props.theme.spaces[6] + 1}px 0px ${props => props.theme.spaces[6] + 1}px 0px; + + @media screen and (min-width: 1500px) { + margin-right: ${props => props.theme.spaces[12] - 1}px; + .bp3-card { + width: ${props => props.theme.card.minWidth}px; + height: ${props => props.theme.card.minHeight}px; + } + } + + @media screen and (min-width: 1500px) and (max-width: 1512px) { + width: ${props => props.theme.card.minWidth + props.theme.spaces[4] * 2}px; + margin-right: ${props => props.theme.spaces[12] - 1}px; + .bp3-card { + width: ${props => props.theme.card.minWidth - 5}px; + height: ${props => props.theme.card.minHeight - 5}px; + } + } + @media screen and (min-width: 1478px) and (max-width: 1500px) { + width: ${props => props.theme.card.minWidth + props.theme.spaces[4] * 2}px; + margin-right: ${props => props.theme.spaces[11] + 1}px; + .bp3-card { + width: ${props => props.theme.card.minWidth - 8}px; + height: ${props => props.theme.card.minHeight - 8}px; + } + } + + @media screen and (min-width: 1447px) and (max-width: 1477px) { + width: ${props => props.theme.card.minWidth + props.theme.spaces[3] * 2}px; + margin-right: ${props => props.theme.spaces[11] - 4}px; + .bp3-card { + width: ${props => props.theme.card.minWidth - 8}px; + height: ${props => props.theme.card.minHeight - 8}px; + } + } + + @media screen and (min-width: 1417px) and (max-width: 1446px) { + width: ${props => props.theme.card.minWidth + props.theme.spaces[3] * 2}px; + margin-right: ${props => props.theme.spaces[11] - 8}px; + .bp3-card { + width: ${props => props.theme.card.minWidth - 11}px; + height: ${props => props.theme.card.minHeight - 11}px; + } + } + + @media screen and (min-width: 1400px) and (max-width: 1417px) { + width: ${props => props.theme.card.minWidth + props.theme.spaces[2] * 2}px; + margin-right: ${props => props.theme.spaces[11] - 12}px; + .bp3-card { + width: ${props => props.theme.card.minWidth - 15}px; + height: ${props => props.theme.card.minHeight - 15}px; + } + } + + @media screen and (max-width: 1400px) { + width: ${props => props.theme.card.minWidth + props.theme.spaces[2] * 2}px; + margin-right: ${props => props.theme.spaces[11] - 16}px; + .bp3-card { + width: ${props => props.theme.card.minWidth - 15}px; + height: ${props => props.theme.card.minHeight - 15}px; + } + } `; const StyledDialog = styled(Dialog)<{ setMaxWidth?: boolean }>` diff --git a/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx b/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx index dcc8ebd72a0..47e1d025ae2 100644 --- a/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx +++ b/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx @@ -9,7 +9,7 @@ import { DataTree, } from "entities/DataTree/dataTreeFactory"; import { useSelector } from "react-redux"; -import { evaluateDataTreeWithoutFunctions } from "selectors/dataTreeSelectors"; +import { getDataTree } from "selectors/dataTreeSelectors"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; @@ -31,7 +31,7 @@ export const EntityProperties = (props: { ); }); let entity: any; - const dataTree: DataTree = useSelector(evaluateDataTreeWithoutFunctions); + const dataTree: DataTree = useSelector(getDataTree); if (props.isCurrentPage && dataTree[props.entityName]) { entity = dataTree[props.entityName]; } else if (props.entity) { @@ -71,7 +71,7 @@ export const EntityProperties = (props: { case ENTITY_TYPE.WIDGET: const type: Exclude< Partial, - "CANVAS_WIDGET" | "ICON_WIDGET" + "CANVAS_WIDGET" | "ICON_WIDGET" | "SKELETON_WIDGET" > = entity.type; config = entityDefinitions[type]; diff --git a/app/client/src/pages/Editor/Explorer/JSDependencies.tsx b/app/client/src/pages/Editor/Explorer/JSDependencies.tsx index c27158e81b9..204518f0725 100644 --- a/app/client/src/pages/Editor/Explorer/JSDependencies.tsx +++ b/app/client/src/pages/Editor/Explorer/JSDependencies.tsx @@ -1,10 +1,10 @@ import React, { useState } from "react"; import styled from "styled-components"; -import { extraLibraries } from "jsExecution/JSExecutionManagerSingleton"; import { Collapse, Icon, IconName, Tooltip } from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; import { Colors } from "constants/Colors"; import { BindingText } from "pages/Editor/APIEditor/Form"; +import { extraLibraries } from "utils/DynamicBindingUtils"; const Wrapper = styled.div` font-size: 13px; diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx index 73f405b7c93..e057047fa5f 100644 --- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx @@ -93,7 +93,7 @@ export const getWidgetProperies = ( entityDefinitions[ widgetProps.type as Exclude< Partial, - "CANVAS_WIDGET" | "ICON_WIDGET" + "CANVAS_WIDGET" | "ICON_WIDGET" | "SKELETON_WIDGET" > ]; diff --git a/app/client/src/pages/Editor/QueryEditor/Form.tsx b/app/client/src/pages/Editor/QueryEditor/Form.tsx index 94868cbb301..d6f87855a2a 100644 --- a/app/client/src/pages/Editor/QueryEditor/Form.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Form.tsx @@ -219,6 +219,7 @@ const TabContainerView = styled.div` .react-tabs__tab-panel { border: 1px solid #ebeff2; + overflow: scroll; } .react-tabs__tab-list { margin: 0px; @@ -226,9 +227,7 @@ const TabContainerView = styled.div` `; const SettingsWrapper = styled.div` - padding-left: 15px; - padding-top: 8px; - padding-bottom: 8px; + padding: 5px 10px; `; const AddWidgetButton = styled(BaseButton)` @@ -246,6 +245,10 @@ const OutputHeader = styled.div` align-items: center; `; +const FieldWrapper = styled.div` + margin-top: 15px; +`; + type QueryFormProps = { onDeleteClick: () => void; onRunClick: () => void; @@ -484,23 +487,28 @@ const QueryEditorForm: React.FC = (props: Props) => { { key: "query", title: "Query", - panelComponent: - editorConfig && editorConfig.length > 0 ? ( - editorConfig.map(renderEachConfig) - ) : ( - <> - An unexpected error occurred - window.location.reload()} - > - Refresh - - - ), + panelComponent: ( + + {editorConfig && editorConfig.length > 0 ? ( + editorConfig.map(renderEachConfig) + ) : ( + <> + + An unexpected error occurred + + window.location.reload()} + > + Refresh + + + )} + + ), }, { key: "settings", @@ -573,13 +581,13 @@ const renderEachConfig = (section: any): any => { try { const { configProperty } = propertyControlOrSection; return ( -
    + {FormControlFactory.createControl( { ...propertyControlOrSection }, {}, false, )} -
    + ); } catch (e) { console.log(e); diff --git a/app/client/src/reducers/entityReducers/metaReducer.ts b/app/client/src/reducers/entityReducers/metaReducer.ts index b94f4af951a..5cd2459cc8c 100644 --- a/app/client/src/reducers/entityReducers/metaReducer.ts +++ b/app/client/src/reducers/entityReducers/metaReducer.ts @@ -2,7 +2,7 @@ import { createReducer } from "utils/AppsmithUtils"; import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; import { UpdateWidgetMetaPropertyPayload } from "actions/metaActions"; -export type MetaState = Record; +export type MetaState = Record>; const initialState: MetaState = {}; diff --git a/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx b/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx index 94a960cf063..c0fdd024aa3 100644 --- a/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx +++ b/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx @@ -26,6 +26,7 @@ import { MapWidgetProps } from "widgets/MapWidget"; import { ModalWidgetProps } from "widgets/ModalWidget"; import { IconWidgetProps } from "widgets/IconWidget"; import { VideoWidgetProps } from "widgets/VideoWidget"; +import { SkeletonWidgetProps } from "../../widgets/SkeletonWidget"; const initialState: WidgetConfigReducerState = WidgetConfigResponse; @@ -74,6 +75,7 @@ export interface WidgetConfigReducerState { CANVAS_WIDGET: Partial> & WidgetConfigProps; ICON_WIDGET: Partial & WidgetConfigProps; + SKELETON_WIDGET: Partial & WidgetConfigProps; }; configVersion: number; } diff --git a/app/client/src/reducers/evalutationReducers/dependencyReducer.ts b/app/client/src/reducers/evalutationReducers/dependencyReducer.ts new file mode 100644 index 00000000000..8800ec8dbb9 --- /dev/null +++ b/app/client/src/reducers/evalutationReducers/dependencyReducer.ts @@ -0,0 +1,21 @@ +import { createReducer } from "utils/AppsmithUtils"; +import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; + +export type EvaluationDependencyState = { + dependencyMap: Record>; + dependencyTree: Array<[string, string]>; +}; + +const initialState: EvaluationDependencyState = { + dependencyMap: {}, + dependencyTree: [], +}; + +const evaluationDependencyReducer = createReducer(initialState, { + [ReduxActionTypes.SET_EVALUATION_DEPENDENCIES]: ( + state: EvaluationDependencyState, + action: ReduxAction, + ) => action.payload, +}); + +export default evaluationDependencyReducer; diff --git a/app/client/src/reducers/evalutationReducers/index.ts b/app/client/src/reducers/evalutationReducers/index.ts new file mode 100644 index 00000000000..926bf4f9997 --- /dev/null +++ b/app/client/src/reducers/evalutationReducers/index.ts @@ -0,0 +1,8 @@ +import { combineReducers } from "redux"; +import evaluatedTreeReducer from "./treeReducer"; +import evaluationDependencyReducer from "./dependencyReducer"; + +export default combineReducers({ + tree: evaluatedTreeReducer, + dependencies: evaluationDependencyReducer, +}); diff --git a/app/client/src/reducers/evalutationReducers/treeReducer.ts b/app/client/src/reducers/evalutationReducers/treeReducer.ts new file mode 100644 index 00000000000..1379cc14e23 --- /dev/null +++ b/app/client/src/reducers/evalutationReducers/treeReducer.ts @@ -0,0 +1,16 @@ +import { createReducer } from "utils/AppsmithUtils"; +import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; + +export type EvaluatedTreeState = DataTree; + +const initialState: EvaluatedTreeState = {}; + +const evaluatedTreeReducer = createReducer(initialState, { + [ReduxActionTypes.SET_EVALUATED_TREE]: ( + state: EvaluatedTreeState, + action: ReduxAction, + ) => action.payload, +}); + +export default evaluatedTreeReducer; diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx index ff3e1317b44..7a3bda7e595 100644 --- a/app/client/src/reducers/index.tsx +++ b/app/client/src/reducers/index.tsx @@ -1,6 +1,7 @@ import { combineReducers } from "redux"; import entityReducer from "./entityReducers"; import uiReducer from "./uiReducers"; +import evaluationsReducer from "./evalutationReducers"; import { reducer as formReducer } from "redux-form"; import { CanvasWidgetsReduxState } from "./entityReducers/canvasWidgetsReducer"; import { EditorReduxState } from "./uiReducers/editorReducer"; @@ -34,10 +35,13 @@ import { PageDSLsReduxState } from "./uiReducers/pageDSLReducer"; import { ConfirmRunActionReduxState } from "./uiReducers/confirmRunActionReducer"; import { AppDataState } from "reducers/entityReducers/appReducer"; import { DatasourceNameReduxState } from "./uiReducers/datasourceNameReducer"; +import { EvaluatedTreeState } from "./evalutationReducers/treeReducer"; +import { EvaluationDependencyState } from "./evalutationReducers/dependencyReducer"; const appReducer = combineReducers({ entities: entityReducer, ui: uiReducer, + evaluations: evaluationsReducer, form: formReducer, }); @@ -80,4 +84,8 @@ export interface AppState { meta: MetaState; app: AppDataState; }; + evaluations: { + tree: EvaluatedTreeState; + dependencies: EvaluationDependencyState; + }; } diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 92a3b6aa6a5..46e69d5ebdb 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -22,15 +22,7 @@ import { takeEvery, takeLatest, } from "redux-saga/effects"; -import { - evaluateDataTreeWithFunctions, - evaluateDataTreeWithoutFunctions, -} from "selectors/dataTreeSelectors"; -import { - getDynamicBindings, - getDynamicValue, - isDynamicValue, -} from "utils/DynamicBindingUtils"; +import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils"; import { ActionDescription, RunActionPayload, @@ -52,8 +44,8 @@ import { import { executeApiActionRequest, executeApiActionSuccess, - updateAction, showRunActionConfirmModal, + updateAction, } from "actions/actionActions"; import { Action, RestAction } from "entities/Action"; import ActionAPI, { @@ -83,6 +75,7 @@ import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; import { getCurrentApplication } from "selectors/applicationSelectors"; +import { evaluateDynamicTrigger, evaluateSingleValue } from "./evaluationsSaga"; function* navigateActionSaga( action: { pageNameOrUrl: string; params: Record }, @@ -204,10 +197,7 @@ const isErrorResponse = (response: ActionApiResponse) => { }; export function* evaluateDynamicBoundValueSaga(path: string): any { - log.debug("Evaluating data tree to get action binding value"); - const tree = yield select(evaluateDataTreeWithoutFunctions); - const dynamicResult = getDynamicValue(`{{${path}}}`, tree); - return dynamicResult.result; + return yield call(evaluateSingleValue, `{{${path}}}`); } const EXECUTION_PARAM_PATH = "this.params"; @@ -483,15 +473,20 @@ function* executeActionTriggers( function* executeAppAction(action: ReduxAction) { const { dynamicString, event, responseData } = action.payload; - log.debug("Evaluating data tree to get action trigger"); - log.debug({ dynamicString }); - const tree = yield select(evaluateDataTreeWithFunctions); - log.debug({ tree }); - const { triggers } = getDynamicValue(dynamicString, tree, responseData, true); + log.debug({ dynamicString, responseData }); + + const triggers = yield call( + evaluateDynamicTrigger, + dynamicString, + responseData, + ); + log.debug({ triggers }); if (triggers && triggers.length) { yield all( - triggers.map(trigger => call(executeActionTriggers, trigger, event)), + triggers.map((trigger: ActionDescription) => + call(executeActionTriggers, trigger, event), + ), ); } else { if (event.callback) event.callback({ success: true }); diff --git a/app/client/src/sagas/BatchSagas.tsx b/app/client/src/sagas/BatchSagas.tsx index effb2100e7f..23654c032e3 100644 --- a/app/client/src/sagas/BatchSagas.tsx +++ b/app/client/src/sagas/BatchSagas.tsx @@ -2,6 +2,7 @@ import _ from "lodash"; import { put, debounce, takeEvery, all } from "redux-saga/effects"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; +import { batchActionSuccess } from "../actions/batchActions"; const BATCH_PRIORITY = { [ReduxActionTypes.SET_META_PROP]: { @@ -62,6 +63,7 @@ function* executeBatchSaga() { yield put(sagaAction); } } + yield put(batchActionSuccess(batch)); } } } diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 97d793e067a..05987e0b1e5 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -11,6 +11,7 @@ import { fetchEditorConfigs } from "actions/configsActions"; import { fetchPage, fetchPageList, + fetchPublishedPage, setAppMode, updateAppStore, } from "actions/pageActions"; @@ -26,6 +27,7 @@ import { validateResponse } from "./ErrorSagas"; import { extractCurrentDSL } from "utils/WidgetPropsUtils"; import { APP_MODE } from "reducers/entityReducers/appReducer"; import { getAppStoreName } from "constants/AppConstants"; +import { getDefaultPageId } from "./selectors"; const getAppStore = (appId: string) => { const appStoreName = getAppStoreName(appId); @@ -146,7 +148,7 @@ export function* populatePageDSLsSaga() { } export function* initializeAppViewerSaga( - action: ReduxAction<{ pageId: string; applicationId: string }>, + action: ReduxAction<{ applicationId: string }>, ) { const { applicationId } = action.payload; yield all([ @@ -160,16 +162,23 @@ export function* initializeAppViewerSaga( take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), ]); - yield put(setAppMode(APP_MODE.PUBLISHED)); - yield put(updateAppStore(getAppStore(applicationId))); + const pageId = yield select(getDefaultPageId); + + if (pageId) { + yield put(fetchPublishedPage(pageId, true)); + yield take(ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS); + + yield put(setAppMode(APP_MODE.PUBLISHED)); + yield put(updateAppStore(getAppStore(applicationId))); - yield put({ - type: ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS, - }); - if ("serviceWorker" in navigator) { yield put({ - type: ReduxActionTypes.FETCH_ALL_PUBLISHED_PAGES, + type: ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS, }); + if ("serviceWorker" in navigator) { + yield put({ + type: ReduxActionTypes.FETCH_ALL_PUBLISHED_PAGES, + }); + } } } diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index 040654e2220..9e0b5a8147b 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -69,7 +69,7 @@ import { fetchActionsForPage, setActionsToExecuteOnPageLoad, } from "actions/actionActions"; -import { clearCaches } from "utils/DynamicBindingUtils"; +import { clearEvalCache } from "./evaluationsSaga"; import { UrlDataState } from "reducers/entityReducers/appReducer"; import { getQueryParams } from "utils/AppsmithUtils"; import PerformanceTracker, { @@ -162,7 +162,7 @@ export function* fetchPageSaga( const isValidResponse = yield validateResponse(fetchPageResponse); if (isValidResponse) { // Clear any existing caches - clearCaches(); + yield call(clearEvalCache); // Set url params yield call(setDataUrl); // Get Canvas payload @@ -225,7 +225,7 @@ export function* fetchPublishedPageSaga( const isValidResponse = yield validateResponse(response); if (isValidResponse) { // Clear any existing caches - clearCaches(); + yield call(clearEvalCache); // Set url params yield call(setDataUrl); // Get Canvas payload diff --git a/app/client/src/sagas/SagaUtils.ts b/app/client/src/sagas/SagaUtils.ts index 7e12889e763..0d5df419e6b 100644 --- a/app/client/src/sagas/SagaUtils.ts +++ b/app/client/src/sagas/SagaUtils.ts @@ -1,4 +1,5 @@ import { ApplicationPagePayload } from "api/ApplicationApi"; + export const getDefaultPageId = ( pages?: ApplicationPagePayload[], ): string | undefined => { diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index f161c10875b..a49ab260b71 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -57,7 +57,6 @@ import { RenderModes, WidgetType, } from "constants/WidgetConstants"; -import ValidationFactory from "utils/ValidationFactory"; import WidgetConfigResponse from "mockResponses/WidgetConfigResponse"; import { saveCopiedWidgets, @@ -78,6 +77,9 @@ import { getCurrentPageId, } from "selectors/editorSelectors"; import { forceOpenPropertyPane } from "actions/widgetActions"; +import { getDataTree } from "selectors/dataTreeSelectors"; +import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; +import { validateProperty } from "./evaluationsSaga"; function getChildWidgetProps( parent: FlattenedWidgetProps, @@ -160,6 +162,7 @@ function* generateChildWidgets( ); } widget.parentId = parent.widgetId; + delete widget.blueprint; return { widgetId: widget.widgetId, widgets }; } @@ -631,7 +634,9 @@ function* setWidgetDynamicPropertySaga( yield put(updateWidgetProperty(widgetId, propertyName, value)); } else { delete dynamicProperties[propertyName]; - const { parsed } = ValidationFactory.validateWidgetProperty( + // TODO (hetu) can we eliminate this use of validation + const { parsed } = yield call( + validateProperty, widget.type, propertyName, propertyValue, @@ -668,6 +673,39 @@ function* resetChildrenMetaSaga(action: ReduxAction<{ widgetId: string }>) { const childId = childrenIds[childIndex]; yield put(resetWidgetMetaProperty(childId)); } + yield call(resetEvaluatedWidgetMetaProperties, childrenIds); +} + +// This is needed because evaluation takes some time and we can reset the props +// in the evaluated value much faster like this +function* resetEvaluatedWidgetMetaProperties(widgetIds: string[]) { + const evaluatedDataTree = yield select(getDataTree); + const updates: Record = {}; + for (const index in widgetIds) { + const widgetId = widgetIds[index]; + const widget = _.find(evaluatedDataTree, { widgetId }) as DataTreeWidget; + const widgetToUpdate = { ...widget }; + const metaPropsMap = WidgetFactory.getWidgetMetaPropertiesMap(widget.type); + const defaultPropertiesMap = WidgetFactory.getWidgetDefaultPropertiesMap( + widget.type, + ); + Object.keys(metaPropsMap).forEach(metaProp => { + if (metaProp in defaultPropertiesMap) { + widgetToUpdate[metaProp] = widget[defaultPropertiesMap[metaProp]]; + } else { + widgetToUpdate[metaProp] = metaPropsMap[metaProp]; + } + }); + updates[widget.widgetName] = widgetToUpdate; + } + const newEvaluatedDataTree = { + ...evaluatedDataTree, + ...updates, + }; + yield put({ + type: ReduxActionTypes.SET_EVALUATED_TREE, + payload: newEvaluatedDataTree, + }); } function* updateCanvasSize( @@ -997,6 +1035,12 @@ function* addTableWidgetFromQuerySaga(action: ReduxAction) { parentRowSpace: 1, parentColumnSpace: 1, isLoading: false, + props: { + tableData: `{{${queryName}.data}}`, + dynamicBindings: { + tableData: true, + }, + }, }; const { leftColumn, @@ -1035,14 +1079,6 @@ function* addTableWidgetFromQuerySaga(action: ReduxAction) { payload: { widgetId: newWidget.newWidgetId }, }); yield put(forceOpenPropertyPane(newWidget.newWidgetId)); - yield put( - updateWidgetPropertyRequest( - newWidget.newWidgetId, - "tableData", - `{{${queryName}.data}}`, - RenderModes.CANVAS, - ), - ); } catch (error) { AppToaster.show({ message: "Failed to add the widget", diff --git a/app/client/src/sagas/evaluationsSaga.ts b/app/client/src/sagas/evaluationsSaga.ts new file mode 100644 index 00000000000..762c5121ec8 --- /dev/null +++ b/app/client/src/sagas/evaluationsSaga.ts @@ -0,0 +1,227 @@ +import { + all, + call, + fork, + put, + select, + take, + takeLatest, +} from "redux-saga/effects"; +import { eventChannel, EventChannel } from "redux-saga"; +import { + ReduxAction, + ReduxActionErrorTypes, + ReduxActionTypes, +} from "constants/ReduxActionConstants"; +import { + getDataTree, + getUnevaluatedDataTree, +} from "selectors/dataTreeSelectors"; +import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory"; +import Worker from "worker-loader!../workers/evaluation.worker"; +import { + EVAL_WORKER_ACTIONS, + EvalError, + EvalErrorTypes, +} from "../utils/DynamicBindingUtils"; +import { ToastType } from "react-toastify"; +import { AppToaster } from "../components/editorComponents/ToastComponent"; +import log from "loglevel"; +import _ from "lodash"; +import { WidgetType } from "../constants/WidgetConstants"; +import { WidgetProps } from "../widgets/BaseWidget"; + +let evaluationWorker: Worker; +let workerChannel: EventChannel; +let widgetTypeConfigMap: WidgetTypeConfigMap; + +const initEvaluationWorkers = () => { + widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap(); + evaluationWorker = new Worker(); + workerChannel = eventChannel(emitter => { + evaluationWorker.addEventListener("message", emitter); + // The subscriber must return an unsubscribe function + return () => { + evaluationWorker.removeEventListener("message", emitter); + }; + }); +}; + +const evalErrorHandler = (errors: EvalError[]) => { + errors.forEach(error => { + if (error.type === EvalErrorTypes.DEPENDENCY_ERROR) { + AppToaster.show({ + message: error.message, + type: ToastType.ERROR, + }); + } + log.debug(error); + }); +}; + +function* evaluateTreeSaga() { + const unEvalTree = yield select(getUnevaluatedDataTree); + log.debug({ unEvalTree }); + evaluationWorker.postMessage({ + action: EVAL_WORKER_ACTIONS.EVAL_TREE, + dataTree: unEvalTree, + widgetTypeConfigMap, + }); + const workerResponse = yield take(workerChannel); + const { errors, dataTree } = workerResponse.data; + const parsedDataTree = JSON.parse(dataTree); + log.debug({ dataTree: parsedDataTree }); + evalErrorHandler(errors); + yield put({ + type: ReduxActionTypes.SET_EVALUATED_TREE, + payload: parsedDataTree, + }); +} + +export function* evaluateSingleValue(binding: string) { + if (evaluationWorker) { + const evalTree = yield select(getDataTree); + evaluationWorker.postMessage({ + action: EVAL_WORKER_ACTIONS.EVAL_SINGLE, + dataTree: evalTree, + binding, + }); + const workerResponse = yield take(workerChannel); + const { errors, value } = workerResponse.data; + evalErrorHandler(errors); + return value; + } +} + +export function* evaluateDynamicTrigger( + dynamicTrigger: string, + callbackData: any, +) { + if (evaluationWorker) { + const unEvalTree = yield select(getUnevaluatedDataTree); + evaluationWorker.postMessage({ + action: EVAL_WORKER_ACTIONS.EVAL_TRIGGER, + dataTree: unEvalTree, + dynamicTrigger, + callbackData, + }); + const workerResponse = yield take(workerChannel); + const { errors, triggers } = workerResponse.data; + evalErrorHandler(errors); + return triggers; + } + return []; +} + +export function* clearEvalCache() { + if (evaluationWorker) { + evaluationWorker.postMessage({ + action: EVAL_WORKER_ACTIONS.CLEAR_CACHE, + }); + yield take(workerChannel); + return true; + } +} + +export function* clearEvalPropertyCache(propertyPath: string) { + if (evaluationWorker) { + evaluationWorker.postMessage({ + action: EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE, + propertyPath, + }); + yield take(workerChannel); + } +} + +export function* validateProperty( + widgetType: WidgetType, + property: string, + value: any, + props: WidgetProps, +) { + if (evaluationWorker) { + evaluationWorker.postMessage({ + action: EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY, + widgetType, + property, + value, + props, + }); + return yield take(workerChannel); + } + return { isValid: true, parsed: value }; +} + +const EVALUATE_REDUX_ACTIONS = [ + // Actions + ReduxActionTypes.FETCH_ACTIONS_SUCCESS, + ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS, + ReduxActionErrorTypes.FETCH_ACTIONS_ERROR, + ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR, + ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS, + ReduxActionTypes.SUBMIT_CURL_FORM_SUCCESS, + ReduxActionTypes.CREATE_ACTION_SUCCESS, + ReduxActionTypes.UPDATE_ACTION_PROPERTY, + ReduxActionTypes.DELETE_ACTION_SUCCESS, + ReduxActionTypes.COPY_ACTION_SUCCESS, + ReduxActionTypes.MOVE_ACTION_SUCCESS, + ReduxActionTypes.RUN_ACTION_REQUEST, + ReduxActionTypes.RUN_ACTION_SUCCESS, + ReduxActionErrorTypes.RUN_ACTION_ERROR, + ReduxActionTypes.EXECUTE_API_ACTION_REQUEST, + ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS, + ReduxActionErrorTypes.EXECUTE_ACTION_ERROR, + // App Data + ReduxActionTypes.SET_APP_MODE, + ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS, + ReduxActionTypes.SET_URL_DATA, + ReduxActionTypes.UPDATE_APP_STORE, + // Widgets + ReduxActionTypes.UPDATE_LAYOUT, + ReduxActionTypes.UPDATE_WIDGET_PROPERTY, + ReduxActionTypes.UPDATE_WIDGET_NAME_SUCCESS, + // Widget Meta + ReduxActionTypes.SET_META_PROP, + ReduxActionTypes.RESET_WIDGET_META, + // Pages + ReduxActionTypes.FETCH_PAGE_SUCCESS, + ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS, + // Batches + ReduxActionTypes.BATCH_UPDATES_SUCCESS, +]; + +function* evaluationChangeListenerSaga() { + initEvaluationWorkers(); + yield call(evaluateTreeSaga); + while (true) { + const action: ReduxAction = yield take(EVALUATE_REDUX_ACTIONS); + // When batching success action happens, we need to only evaluate + // if the batch had any action we need to evaluate properties for + if (action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS) { + const batchedActionTypes = action.payload.map( + (batchedAction: ReduxAction) => batchedAction.type, + ); + if ( + _.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length === 0 + ) { + continue; + } + } + log.debug(`Evaluating`, { action }); + yield fork(evaluateTreeSaga); + } + // TODO(hetu) need an action to stop listening and evaluate (exit app) +} + +export default function* evaluationSagaListeners() { + yield all([ + takeLatest( + ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, + evaluationChangeListenerSaga, + ), + takeLatest( + ReduxActionTypes.INITIALIZE_PAGE_VIEWER_SUCCESS, + evaluationChangeListenerSaga, + ), + ]); +} diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index a246e6922fe..1177539f1c8 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -19,6 +19,7 @@ import queryPaneSagas from "./QueryPaneSagas"; import modalSagas from "./ModalSagas"; import batchSagas from "./BatchSagas"; import themeSagas from "./ThemeSaga"; +import evaluationsSaga from "./evaluationsSaga"; export function* rootSaga() { yield all([ @@ -42,5 +43,6 @@ export function* rootSaga() { spawn(modalSagas), spawn(batchSagas), spawn(themeSagas), + spawn(evaluationsSaga), ]); } diff --git a/app/client/src/sagas/selectors.tsx b/app/client/src/sagas/selectors.tsx index 0adb270cdb0..ec2aa857690 100644 --- a/app/client/src/sagas/selectors.tsx +++ b/app/client/src/sagas/selectors.tsx @@ -55,11 +55,8 @@ export const getWidgetNamePrefix = ( return state.entities.widgetConfig.config[type].widgetName; }; -export const getDefaultPageId = (state: AppState, pageId?: string): string => { - const { pages } = state.entities.pageList; - const page = pages.find(page => page.pageId === pageId); - return page ? page.pageId : pages[0].pageId; -}; +export const getDefaultPageId = (state: AppState): string | undefined => + state.entities.pageList.defaultPageId; export const getExistingWidgetNames = createSelector( getWidgets, diff --git a/app/client/src/selectors/dataTreeSelectors.ts b/app/client/src/selectors/dataTreeSelectors.ts index 7b2dc945cdb..eabc38fe135 100644 --- a/app/client/src/selectors/dataTreeSelectors.ts +++ b/app/client/src/selectors/dataTreeSelectors.ts @@ -1,7 +1,6 @@ import { createSelector } from "reselect"; import { getActionsForCurrentPage, getAppData } from "./entitiesSelector"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; -import { getEvaluatedDataTree } from "utils/DynamicBindingUtils"; import { DataTree, DataTreeFactory } from "entities/DataTree/dataTreeFactory"; import { getWidgets, getWidgetsMeta } from "sagas/selectors"; import * as log from "loglevel"; @@ -10,6 +9,7 @@ import { getPageList } from "./appViewSelectors"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; +import { AppState } from "../reducers"; export const getUnevaluatedDataTree = createSelector( getActionsForCurrentPage, @@ -22,43 +22,26 @@ export const getUnevaluatedDataTree = createSelector( PerformanceTransactionName.CONSTRUCT_UNEVAL_TREE, ); const pageList = pageListPayload || []; - const unevalTree = DataTreeFactory.create( - { - actions, - widgets, - widgetsMeta, - pageList, - appData, - }, - true, - ); + const unevalTree = DataTreeFactory.create({ + actions, + widgets, + widgetsMeta, + pageList, + appData, + }); PerformanceTracker.stopTracking(); return unevalTree; }, ); -export const evaluateDataTree = createSelector( - getUnevaluatedDataTree, - (dataTree: DataTree): DataTree => { - PerformanceTracker.startTracking( - PerformanceTransactionName.DATA_TREE_EVALUATION, - ); - const evalDataTree = getEvaluatedDataTree(dataTree); - PerformanceTracker.stopTracking(); - return evalDataTree; - }, -); - -export const evaluateDataTreeWithFunctions = evaluateDataTree; -export const evaluateDataTreeWithoutFunctions = evaluateDataTree; +export const getDataTree = (state: AppState) => state.evaluations.tree; // For autocomplete. Use actions cached responses if // there isn't a response already export const getDataTreeForAutocomplete = createSelector( - evaluateDataTreeWithoutFunctions, + getDataTree, getActionsForCurrentPage, (tree: DataTree, actions: ActionDataState) => { - log.debug("Evaluating data tree to get autocomplete values"); const cachedResponses: Record = {}; if (actions && actions.length) { actions.forEach(action => { diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index f81a924fb0b..1f1c4da8450 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -2,27 +2,33 @@ import { createSelector } from "reselect"; import { AppState } from "reducers"; import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer"; -import { WidgetCardProps, WidgetProps } from "widgets/BaseWidget"; +import { + WIDGET_STATIC_PROPS, + WidgetCardProps, + WidgetProps, +} from "widgets/BaseWidget"; import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; -import { getEntities } from "./entitiesSelector"; import { - FlattenedWidgetProps, CanvasWidgetsReduxState, + FlattenedWidgetProps, } from "reducers/entityReducers/canvasWidgetsReducer"; import { PageListReduxState } from "reducers/entityReducers/pageListReducer"; import { OccupiedSpace } from "constants/editorConstants"; -import { evaluateDataTreeWithoutFunctions } from "selectors/dataTreeSelectors"; +import { getDataTree } from "selectors/dataTreeSelectors"; import _ from "lodash"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; -import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; -import { getActions } from "sagas/selectors"; +import { DataTreeWidget, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import { getActions, getWidgetsMeta } from "sagas/selectors"; import * as log from "loglevel"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; +import { getCanvasWidgets } from "./entitiesSelector"; +import { MetaState } from "../reducers/entityReducers/metaReducer"; +import { WidgetTypes } from "../constants/WidgetConstants"; const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; const getWidgetSideBar = (state: AppState) => state.ui.widgetSidebar; @@ -104,26 +110,28 @@ export const getWidgetCards = createSelector( ); export const getCanvasWidgetDsl = createSelector( - getEntities, - evaluateDataTreeWithoutFunctions, + getCanvasWidgets, + getDataTree, ( - entities: AppState["entities"], + canvasWidgets: CanvasWidgetsReduxState, evaluatedDataTree, ): ContainerWidgetProps => { PerformanceTracker.startTracking( PerformanceTransactionName.CONSTRUCT_CANVAS_DSL, ); - log.debug("Evaluating data tree to get canvas widgets"); - log.debug({ evaluatedDataTree }); - const widgets = { ...entities.canvasWidgets }; - Object.keys(widgets).forEach(widgetKey => { - const evaluatedWidget = _.find(evaluatedDataTree, { - widgetId: widgetKey, - }); + const widgets: Record = {}; + Object.keys(canvasWidgets).forEach(widgetKey => { + const canvasWidget = canvasWidgets[widgetKey]; + const evaluatedWidget = evaluatedDataTree[ + canvasWidget.widgetName + ] as DataTreeWidget; if (evaluatedWidget) { - widgets[widgetKey] = evaluatedWidget as DataTreeWidget; + widgets[widgetKey] = createCanvasWidget(canvasWidget, evaluatedWidget); + } else { + widgets[widgetKey] = createLoadingWidget(canvasWidget); } }); + const denormalizedWidgets = CanvasWidgetsNormalizer.denormalize("0", { canvasWidgets: widgets, }); @@ -197,3 +205,32 @@ export const getActionById = createSelector( } }, ); + +const createCanvasWidget = ( + canvasWidget: FlattenedWidgetProps, + evaluatedWidget: DataTreeWidget, +) => { + const widgetStaticProps = _.pick( + canvasWidget, + Object.keys(WIDGET_STATIC_PROPS), + ); + return { + ...evaluatedWidget, + ...widgetStaticProps, + }; +}; + +const createLoadingWidget = ( + canvasWidget: FlattenedWidgetProps, +): DataTreeWidget => { + const widgetStaticProps = _.pick( + canvasWidget, + Object.keys(WIDGET_STATIC_PROPS), + ) as WidgetProps; + return { + ...widgetStaticProps, + type: WidgetTypes.SKELETON_WIDGET, + ENTITY_TYPE: ENTITY_TYPE.WIDGET, + isLoading: true, + }; +}; diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 8a9e9eec76b..be0ca0a4ea6 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -10,6 +10,7 @@ import { Datasource } from "api/DatasourcesApi"; import { Action } from "entities/Action"; import { find } from "lodash"; import ImageAlt from "assets/images/placeholder-image.svg"; +import { CanvasWidgetsReduxState } from "../reducers/entityReducers/canvasWidgetsReducer"; export const getEntities = (state: AppState): AppState["entities"] => state.entities; @@ -267,3 +268,6 @@ export const isActionDirty = (id: string) => }); export const getAppData = (state: AppState) => state.entities.app; + +export const getCanvasWidgets = (state: AppState): CanvasWidgetsReduxState => + state.entities.canvasWidgets; diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index 05350d5e4f5..581d5bc3339 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -1,14 +1,17 @@ import { createSelector } from "reselect"; import { AppState } from "reducers"; import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer"; -import { PropertyPaneConfigState } from "reducers/entityReducers/propertyPaneConfigReducer"; +import { + PropertyPaneConfigState, + PropertySection, +} from "reducers/entityReducers/propertyPaneConfigReducer"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; -import { PropertySection } from "reducers/entityReducers/propertyPaneConfigReducer"; import { WidgetProps } from "widgets/BaseWidget"; import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory"; import _ from "lodash"; -import { evaluateDataTreeWithoutFunctions } from "selectors/dataTreeSelectors"; +import { getDataTree } from "selectors/dataTreeSelectors"; import * as log from "loglevel"; +import { getCanvasWidgets } from "./entitiesSelector"; const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => state.ui.propertyPane; @@ -16,9 +19,6 @@ const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => const getPropertyPaneConfig = (state: AppState): PropertyPaneConfigState => state.entities.propertyConfig; -const getCanvasWidgets = (state: AppState): CanvasWidgetsReduxState => - state.entities.canvasWidgets; - export const getCurrentWidgetId = createSelector( getPropertyPaneState, (propertyPane: PropertyPaneReduxState) => propertyPane.widgetId, @@ -37,12 +37,11 @@ export const getCurrentWidgetProperties = createSelector( export const getWidgetPropsForPropertyPane = createSelector( getCurrentWidgetProperties, - evaluateDataTreeWithoutFunctions, + getDataTree, ( widget: WidgetProps | undefined, evaluatedTree: DataTree, ): WidgetProps | undefined => { - log.debug("Evaluating data tree to get property pane validations"); if (!widget) return undefined; const evaluatedWidget = _.find(evaluatedTree, { widgetId: widget.widgetId, diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 820e44672d2..cbc38f3a96e 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -1,28 +1,11 @@ -import _ from "lodash"; +import _, { VERSION as lodashVersion } from "lodash"; import { DATA_BIND_REGEX, DATA_BIND_REGEX_GLOBAL, } from "constants/BindingsConstants"; -import ValidationFactory from "./ValidationFactory"; -import JSExecutionManagerSingleton, { - JSExecutorResult, -} from "jsExecution/JSExecutionManagerSingleton"; -import unescapeJS from "unescape-js"; -import toposort from "toposort"; -import { - DataTree, - DataTreeEntity, - DataTreeWidget, - ENTITY_TYPE, -} from "entities/DataTree/dataTreeFactory"; -import equal from "fast-deep-equal/es6"; -import WidgetFactory from "utils/WidgetFactory"; -import { AppToaster } from "components/editorComponents/ToastComponent"; -import { ToastType } from "react-toastify"; import { Action } from "entities/Action"; -import PerformanceTracker, { - PerformanceTransactionName, -} from "utils/PerformanceTracker"; +import moment from "moment-timezone"; +import { atob, btoa, version as BASE64LIBVERSION } from "js-base64"; type StringTuple = [string, string]; @@ -77,27 +60,6 @@ export function getDynamicStringSegments(dynamicString: string): string[] { return stringSegments; } -const getAllPaths = ( - tree: Record, - prefix = "", -): Record => { - return Object.keys(tree).reduce((res: Record, el): Record< - string, - true - > => { - if (Array.isArray(tree[el])) { - const key = `${prefix}${el}`; - return { ...res, [key]: true }; - } else if (typeof tree[el] === "object" && tree[el] !== null) { - const key = `${prefix}${el}`; - return { ...res, [key]: true, ...getAllPaths(tree[el], `${key}.`) }; - } else { - const key = `${prefix}${el}`; - return { ...res, [key]: true }; - } - }, {}); -}; - export const getDynamicBindings = ( dynamicString: string, ): { stringSegments: string[]; jsSnippets: string[] } => { @@ -120,671 +82,64 @@ export const getDynamicBindings = ( return { stringSegments: stringSegments, jsSnippets: paths }; }; -// Paths are expected to have "{name}.{path}" signature -// Also returns any action triggers found after evaluating value -export const evaluateDynamicBoundValue = ( - data: DataTree, - path: string, - callbackData?: any, -): JSExecutorResult => { - const unescapedJS = unescapeJS(path).replace(/(\r\n|\n|\r)/gm, ""); - return JSExecutionManagerSingleton.evaluateSync( - unescapedJS, - data, - callbackData, - ); -}; - -// For creating a final value where bindings could be in a template format -export const createDynamicValueString = ( - binding: string, - subBindings: string[], - subValues: string[], -): string => { - // Replace the string with the data tree values - let finalValue = binding; - subBindings.forEach((b, i) => { - let value = subValues[i]; - if (Array.isArray(value) || _.isObject(value)) { - value = JSON.stringify(value); - } - try { - if (JSON.parse(value)) { - value = value.replace(/\\([\s\S])|(")/g, "\\$1$2"); - } - } catch (e) { - // do nothing - } - finalValue = finalValue.replace(b, value); - }); - return finalValue; -}; - -export const getDynamicValue = ( - dynamicBinding: string, - data: DataTree, - callBackData?: any, - includeTriggers = false, -): JSExecutorResult => { - // Get the {{binding}} bound values - const { stringSegments, jsSnippets } = getDynamicBindings(dynamicBinding); - if (stringSegments.length) { - // Get the Data Tree value of those "binding "paths - const values = jsSnippets.map((jsSnippet, index) => { - if (jsSnippet) { - const result = evaluateDynamicBoundValue(data, jsSnippet, callBackData); - if (includeTriggers) { - return result; - } else { - return { result: result.result }; - } - } else { - return { result: stringSegments[index], triggers: [] }; - } - }); - - // if it is just one binding, no need to create template string - if (stringSegments.length === 1) return values[0]; - // else return a string template with bindings - const templateString = createDynamicValueString( - dynamicBinding, - stringSegments, - values.map(v => v.result), - ); - return { - result: templateString, - }; - } - return { result: undefined, triggers: [] }; -}; - -export const getValidatedTree = (tree: any) => { - return Object.keys(tree).reduce((tree, entityKey: string) => { - const entity = tree[entityKey]; - if (entity && entity.type) { - const parsedEntity = { ...entity }; - Object.keys(entity).forEach((property: string) => { - const hasEvaluatedValue = _.has( - parsedEntity, - `evaluatedValues.${property}`, - ); - const hasValidation = _.has(parsedEntity, `invalidProps.${property}`); - const isSpecialField = [ - "dynamicBindings", - "dynamicTriggers", - "dynamicProperties", - "evaluatedValues", - "invalidProps", - "validationMessages", - ].includes(property); - const isDynamicField = - _.has(parsedEntity, `dynamicBindings.${property}`) || - _.has(parsedEntity, `dynamicTriggers.${property}`); - - if ( - !isSpecialField && - !isDynamicField && - (!hasValidation || !hasEvaluatedValue) - ) { - const value = entity[property]; - // Pass it through parse - const { - parsed, - isValid, - message, - transformed, - } = ValidationFactory.validateWidgetProperty( - entity.type, - property, - value, - entity, - tree, - ); - parsedEntity[property] = parsed; - if (!hasEvaluatedValue) { - const evaluatedValue = isValid - ? parsed - : _.isUndefined(transformed) - ? value - : transformed; - const safeEvaluatedValue = removeFunctions(evaluatedValue); - _.set( - parsedEntity, - `evaluatedValues.${property}`, - safeEvaluatedValue, - ); - } - - const hasValidation = _.has(parsedEntity, `invalidProps.${property}`); - if (!hasValidation && !isValid) { - _.set(parsedEntity, `invalidProps.${property}`, true); - _.set(parsedEntity, `validationMessages.${property}`, message); - } - } - }); - return { ...tree, [entityKey]: parsedEntity }; - } - return tree; - }, tree); -}; - -let dependencyTreeCache: any = {}; -let cachedDataTreeString = ""; - -export function getEvaluatedDataTree(dataTree: DataTree): DataTree { - // Create Dependencies DAG - const dataTreeString = JSON.stringify(dataTree); - // Stringify before doing a fast equals because the data tree has functions and fast equal will always treat those as changed values - // Better solve will be to prune functions - const shouldCreateDependencyTree = !equal( - dataTreeString, - cachedDataTreeString, - ); - PerformanceTracker.startTracking( - PerformanceTransactionName.CREATE_DEPENDENCIES, - { isCacheMiss: shouldCreateDependencyTree }, - ); - if (shouldCreateDependencyTree) { - cachedDataTreeString = dataTreeString; - dependencyTreeCache = createDependencyTree(dataTree); - } - const { - dependencyMap, - sortedDependencies, - dependencyTree, - } = dependencyTreeCache; - PerformanceTracker.stopTracking(); - // Evaluate Tree - PerformanceTracker.startTracking( - PerformanceTransactionName.SORTED_DEPENDENCY_EVALUATION, - { - dependencies: sortedDependencies, - dependencyCount: sortedDependencies.length, - dataTreeSize: cachedDataTreeString.length, - }, - ); - const evaluatedTree = dependencySortedEvaluateDataTree( - dataTree, - dependencyMap, - sortedDependencies, - ); - PerformanceTracker.stopTracking(); - - // Set Loading Widgets - PerformanceTracker.startTracking( - PerformanceTransactionName.SET_WIDGET_LOADING, - ); - const treeWithLoading = setTreeLoading(evaluatedTree, dependencyTree); - PerformanceTracker.stopTracking(); - - // Validate Widgets - PerformanceTracker.startTracking( - PerformanceTransactionName.VALIDATE_DATA_TREE, - ); - const validated = getValidatedTree(treeWithLoading); - PerformanceTracker.stopTracking(); - // dataTreeCache = validated; - return validated; +export enum EvalErrorTypes { + DEPENDENCY_ERROR = "DEPENDENCY_ERROR", + EVAL_PROPERTY_ERROR = "EVAL_PROPERTY_ERROR", + EVAL_TREE_ERROR = "EVAL_TREE_ERROR", + UNESCAPE_STRING_ERROR = "UNESCAPE_STRING_ERROR", + EVAL_ERROR = "EVAL_ERROR", } -type DynamicDependencyMap = Record>; -export const createDependencyTree = ( - dataTree: DataTree, -): { - sortedDependencies: Array; - dependencyTree: Array; - dependencyMap: DynamicDependencyMap; -} => { - const dependencyMap: DynamicDependencyMap = {}; - const allKeys = getAllPaths(dataTree); - Object.keys(dataTree).forEach(entityKey => { - const entity = dataTree[entityKey]; - if (entity && "ENTITY_TYPE" in entity) { - if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { - // Set default property dependency - const defaultProperties = WidgetFactory.getWidgetDefaultPropertiesMap( - entity.type, - ); - Object.keys(defaultProperties).forEach(property => { - dependencyMap[`${entityKey}.${property}`] = [ - `${entityKey}.${defaultProperties[property]}`, - ]; - }); - if (entity.dynamicBindings) { - Object.keys(entity.dynamicBindings).forEach(propertyName => { - // using unescape to remove new lines from bindings which interfere with our regex extraction - const unevalPropValue = _.get(entity, propertyName); - const { jsSnippets } = getDynamicBindings(unevalPropValue); - const existingDeps = - dependencyMap[`${entityKey}.${propertyName}`] || []; - dependencyMap[`${entityKey}.${propertyName}`] = existingDeps.concat( - jsSnippets.filter(jsSnippet => !!jsSnippet), - ); - }); - } - if (entity.dynamicTriggers) { - Object.keys(entity.dynamicTriggers).forEach(prop => { - dependencyMap[`${entityKey}.${prop}`] = []; - }); - } - } - if (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION) { - if (entity.dynamicBindingPathList.length) { - entity.dynamicBindingPathList.forEach(prop => { - // using unescape to remove new lines from bindings which interfere with our regex extraction - const unevalPropValue = _.get(entity, prop.key); - const { jsSnippets } = getDynamicBindings(unevalPropValue); - const existingDeps = - dependencyMap[`${entityKey}.${prop.key}`] || []; - dependencyMap[`${entityKey}.${prop.key}`] = existingDeps.concat( - jsSnippets.filter(jsSnippet => !!jsSnippet), - ); - }); - } - } - } - }); - Object.keys(dependencyMap).forEach(key => { - dependencyMap[key] = _.flatten( - dependencyMap[key].map(path => calculateSubDependencies(path, allKeys)), - ); - }); - const dependencyTree: Array = []; - Object.keys(dependencyMap).forEach((key: string) => { - if (dependencyMap[key].length) { - dependencyMap[key].forEach(dep => dependencyTree.push([key, dep])); - } else { - // Set no dependency - dependencyTree.push([key, ""]); - } - }); - - try { - // sort dependencies and remove empty dependencies - const sortedDependencies = toposort(dependencyTree) - .reverse() - .filter(d => !!d); - - return { sortedDependencies, dependencyMap, dependencyTree }; - } catch (e) { - console.error(e); - AppToaster.show({ - message: e.message, - type: ToastType.ERROR, - }); - return { sortedDependencies: [], dependencyMap: {}, dependencyTree: [] }; - } -}; - -const calculateSubDependencies = ( - path: string, - all: Record, -): Array => { - const subDeps: Array = []; - const identifiers = path.match(/[a-zA-Z_$][a-zA-Z_$0-9.]*/g) || [path]; - identifiers.forEach((identifier: string) => { - if (all.hasOwnProperty(identifier)) { - subDeps.push(identifier); - } else { - const subIdentifiers = - identifier.match(/[a-zA-Z_$][a-zA-Z_$0-9]*/g) || []; - let current = ""; - for (let i = 0; i < subIdentifiers.length; i++) { - const key = `${current}${current ? "." : ""}${subIdentifiers[i]}`; - if (key in all) { - current = key; - } else { - break; - } - } - if (current && current.includes(".")) subDeps.push(current); - } - }); - return _.uniq(subDeps); -}; - -export const setTreeLoading = ( - dataTree: DataTree, - dependencyMap: Array, -) => { - const widgets: string[] = []; - const isLoadingActions: string[] = []; - - // Fetch all actions that are in loading state - Object.keys(dataTree).forEach(e => { - const entity = dataTree[e]; - if (entity && "ENTITY_TYPE" in entity) { - if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { - widgets.push(e); - } else if ( - entity.ENTITY_TYPE === ENTITY_TYPE.ACTION && - entity.isLoading - ) { - isLoadingActions.push(e); - } - } - }); - - // get all widget dependencies of those actions - isLoadingActions - .reduce( - (allEntities: string[], curr) => - allEntities.concat(getEntityDependencies(dependencyMap, curr, widgets)), - [], - ) - // set loading to true for those widgets - .forEach(w => { - const entity = dataTree[w] as DataTreeWidget; - entity.isLoading = true; - }); - return dataTree; +export type EvalError = { + type: EvalErrorTypes; + message: string; + context?: Record; }; -export const getEntityDependencies = ( - dependencyMap: Array, - entity: string, - entities: string[], -): Array => { - const entityDeps: Record = dependencyMap - .map(d => [d[1].split(".")[0], d[0].split(".")[0]]) - .filter(d => d[0] !== d[1]) - .reduce((deps: Record, dep) => { - const key: string = dep[0]; - const value: string = dep[1]; - return { - ...deps, - [key]: deps[key] ? deps[key].concat(value) : [value], - }; - }, {}); +export enum EVAL_WORKER_ACTIONS { + EVAL_TREE = "EVAL_TREE", + EVAL_SINGLE = "EVAL_SINGLE", + EVAL_TRIGGER = "EVAL_TRIGGER", + CLEAR_PROPERTY_CACHE = "CLEAR_PROPERTY_CACHE", + CLEAR_CACHE = "CLEAR_CACHE", + VALIDATE_PROPERTY = "VALIDATE_PROPERTY", +} - if (entity in entityDeps) { - const recFind = ( - keys: Array, - deps: Record, - ): Array => { - let allDeps: string[] = []; - keys - .filter(k => entities.includes(k)) - .forEach(e => { - allDeps = allDeps.concat([e]); - if (e in deps) { - allDeps = allDeps.concat([...recFind(deps[e], deps)]); - } - }); - return allDeps; - }; - return recFind(entityDeps[entity], entityDeps); - } - return []; +export type ExtraLibrary = { + version: string; + docsURL: string; + displayName: string; + accessor: string; + lib: any; }; -const dynamicPropValueCache: Map< - string, +export const extraLibraries: ExtraLibrary[] = [ { - unEvaluated: any; - evaluated: any; - } -> = new Map(); - -const parsedValueCache: Map< - string, + accessor: "_", + lib: _, + version: lodashVersion, + docsURL: `https://lodash.com/docs/${lodashVersion}`, + displayName: "lodash", + }, { - value: any; - version: number; - } -> = new Map(); - -const getDynamicPropValueCache = (propertyPath: string) => - dynamicPropValueCache.get(propertyPath); - -const getParsedValueCache = (propertyPath: string) => - parsedValueCache.get(propertyPath) || { - value: undefined, - version: 0, - }; - -export const clearPropertyCache = (propertyPath: string) => - parsedValueCache.delete(propertyPath); - -const dependencyCache: Map = new Map(); - -export const clearCaches = () => { - dynamicPropValueCache.clear(); - dependencyCache.clear(); - parsedValueCache.clear(); -}; - -function getCurrentDependencyValues( - propertyDependencies: Array, - currentTree: DataTree, - currentPropertyPath: string, -): Array { - return propertyDependencies - ? propertyDependencies - .map((path: string) => { - //*** Remove current path from data tree because cached value contains evaluated version while this contains unevaluated version */ - const cleanDataTree = _.omit(currentTree, [currentPropertyPath]); - return _.get(cleanDataTree, path); - }) - .filter((data: any) => { - return data !== undefined; - }) - : []; -} - -function evaluateDynamicProperty( - propertyPath: string, - currentTree: DataTree, - unEvalPropertyValue: any, - currentDependencyValues: Array, - cachedDependencyValues?: Array, -): any { - const cacheObj = getDynamicPropValueCache(propertyPath); - const isCacheHit = - cacheObj && - equal(cacheObj.unEvaluated, unEvalPropertyValue) && - cachedDependencyValues !== undefined && - equal(currentDependencyValues, cachedDependencyValues); - if (isCacheHit && cacheObj) { - return cacheObj.evaluated; - } else { - const dynamicResult = getDynamicValue(unEvalPropertyValue, currentTree); - dynamicPropValueCache.set(propertyPath, { - evaluated: dynamicResult.result, - unEvaluated: unEvalPropertyValue, - }); - dependencyCache.set(propertyPath, currentDependencyValues); - return dynamicResult.result; - } -} - -function validateAndParseWidgetProperty( - propertyPath: string, - widget: DataTreeWidget, - currentTree: DataTree, - evalPropertyValue: any, - unEvalPropertyValue: string, - currentDependencyValues: Array, - cachedDependencyValues?: Array, -): any { - const propertyName = propertyPath.split(".")[1]; - let valueToValidate = evalPropertyValue; - if (widget.dynamicTriggers && propertyName in widget.dynamicTriggers) { - const { triggers } = getDynamicValue( - unEvalPropertyValue, - currentTree, - undefined, - true, - ); - valueToValidate = triggers; - } - const { - parsed, - isValid, - message, - transformed, - } = ValidationFactory.validateWidgetProperty( - widget.type, - propertyName, - valueToValidate, - widget, - currentTree, - ); - const evaluatedValue = isValid - ? parsed - : _.isUndefined(transformed) - ? evalPropertyValue - : transformed; - const safeEvaluatedValue = removeFunctions(evaluatedValue); - _.set(widget, `evaluatedValues.${propertyName}`, safeEvaluatedValue); - if (!isValid) { - _.set(widget, `invalidProps.${propertyName}`, true); - _.set(widget, `validationMessages.${propertyName}`, message); - } - if (widget.dynamicTriggers && propertyName in widget.dynamicTriggers) { - return unEvalPropertyValue; - } else { - const parsedCache = getParsedValueCache(propertyPath); - if ( - !equal(parsedCache.value, parsed) || - (cachedDependencyValues !== undefined && - !equal(currentDependencyValues, cachedDependencyValues)) - ) { - parsedValueCache.set(propertyPath, { - value: parsed, - version: Date.now(), - }); - } - return parsed; - } -} - -function isWidget(entity: DataTreeEntity): boolean { - return "ENTITY_TYPE" in entity && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET; -} - -export function dependencySortedEvaluateDataTree( - dataTree: DataTree, - dependencyMap: DynamicDependencyMap, - sortedDependencies: Array, -): DataTree { - const tree = _.cloneDeep(dataTree); - try { - return sortedDependencies.reduce( - (currentTree: DataTree, propertyPath: string) => { - // PerformanceTracker.startTracking(PerformanceTransactionName.EVALUATE_BINDING, { binding: propertyPath }, true) - const entityName = propertyPath.split(".")[0]; - const entity: DataTreeEntity = currentTree[entityName]; - const unEvalPropertyValue = _.get(currentTree as any, propertyPath); - let evalPropertyValue; - const propertyDependencies = dependencyMap[propertyPath]; - const currentDependencyValues = getCurrentDependencyValues( - propertyDependencies, - currentTree, - propertyPath, - ); - const cachedDependencyValues = dependencyCache.get(propertyPath); - const requiresEval = isDynamicValue(unEvalPropertyValue); - if (requiresEval) { - try { - evalPropertyValue = evaluateDynamicProperty( - propertyPath, - currentTree, - unEvalPropertyValue, - currentDependencyValues, - cachedDependencyValues, - ); - } catch (e) { - console.error(e); - evalPropertyValue = undefined; - } - } else { - evalPropertyValue = unEvalPropertyValue; - // If we have stored any previous dependency cache, clear it - // since it is no longer a binding - if (cachedDependencyValues && cachedDependencyValues.length) { - dependencyCache.set(propertyPath, []); - } - } - if (isWidget(entity)) { - const widgetEntity: DataTreeWidget = entity as DataTreeWidget; - const propertyName = propertyPath.split(".")[1]; - if (propertyName) { - let parsedValue = validateAndParseWidgetProperty( - propertyPath, - widgetEntity, - currentTree, - evalPropertyValue, - unEvalPropertyValue, - currentDependencyValues, - cachedDependencyValues, - ); - const defaultPropertyMap = WidgetFactory.getWidgetDefaultPropertiesMap( - widgetEntity.type, - ); - const hasDefaultProperty = propertyName in defaultPropertyMap; - if (hasDefaultProperty) { - const defaultProperty = defaultPropertyMap[propertyName]; - parsedValue = overwriteDefaultDependentProps( - defaultProperty, - parsedValue, - propertyPath, - widgetEntity, - ); - } - // PerformanceTracker.stopTracking(); - return _.set(currentTree, propertyPath, parsedValue); - } - // PerformanceTracker.stopTracking(); - return _.set(currentTree, propertyPath, evalPropertyValue); - } else { - // PerformanceTracker.stopTracking(); - return _.set(currentTree, propertyPath, evalPropertyValue); - } - }, - tree, - ); - } catch (e) { - console.error(e); - return tree; - } -} - -const overwriteDefaultDependentProps = ( - defaultProperty: string, - propertyValue: any, - propertyPath: string, - entity: DataTreeWidget, -) => { - const defaultPropertyCache = getParsedValueCache( - `${entity.widgetName}.${defaultProperty}`, - ); - const propertyCache = getParsedValueCache(propertyPath); - if ( - propertyValue === undefined || - propertyCache.version < defaultPropertyCache.version - ) { - return defaultPropertyCache.value; - } - return propertyValue; -}; - -// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing -// Check issue https://github.com/appsmithorg/appsmith/issues/719 -const removeFunctions = (value: any) => { - if (_.isFunction(value)) { - return "Function call"; - } else if (_.isObject(value) && _.some(value, _.isFunction)) { - return JSON.parse(JSON.stringify(value)); - } else { - return value; - } -}; - -/* - - Need to evaluated values - Need to validate widget values - Need to replace with default values - - */ + accessor: "moment", + lib: moment, + version: moment.version, + docsURL: `https://momentjs.com/docs/`, + displayName: "moment", + }, + { + accessor: "btoa", + lib: btoa, + version: BASE64LIBVERSION, + docsURL: "https://github.com/dankogai/js-base64#readme", + displayName: "btoa", + }, + { + accessor: "atob", + lib: atob, + version: BASE64LIBVERSION, + docsURL: "https://github.com/dankogai/js-base64#readme", + displayName: "atob", + }, +]; diff --git a/app/client/src/utils/DynamicBindingsUtil.test.ts b/app/client/src/utils/DynamicBindingsUtil.test.ts index 09828596fbb..44d45b60a55 100644 --- a/app/client/src/utils/DynamicBindingsUtil.test.ts +++ b/app/client/src/utils/DynamicBindingsUtil.test.ts @@ -1,4 +1,3 @@ -// // import RealmExecutor from "jsExecution/RealmExecutor"; // import { // mockExecute, // mockRegisterLibrary, @@ -12,12 +11,6 @@ // import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; // import { RenderModes, WidgetTypes } from "constants/WidgetConstants"; // -// jest.mock("jsExecution/RealmExecutor", () => { -// return jest.fn().mockImplementation(() => { -// return { execute: mockExecute, registerLibrary: mockRegisterLibrary }; -// }); -// }); -// // beforeAll(() => { // mockRegisterLibrary.mockClear(); // mockExecute.mockClear(); diff --git a/app/client/src/utils/EditorUtils.ts b/app/client/src/utils/EditorUtils.ts index d2ade871a1e..8e9e202631e 100644 --- a/app/client/src/utils/EditorUtils.ts +++ b/app/client/src/utils/EditorUtils.ts @@ -1,10 +1,9 @@ import WidgetBuilderRegistry from "./WidgetRegistry"; import PropertyControlRegistry from "./PropertyControlRegistry"; -import ValidationRegistry from "./ValidationRegistry"; + export const editorInitializer = async () => { WidgetBuilderRegistry.registerWidgetBuilders(); PropertyControlRegistry.registerPropertyControlBuilders(); - ValidationRegistry.registerInternalValidators(); const moment = await import("moment-timezone"); moment.tz.setDefault(moment.tz.guess()); diff --git a/app/client/src/utils/ValidationFactory.ts b/app/client/src/utils/ValidationFactory.ts deleted file mode 100644 index 94b01d7f5a4..00000000000 --- a/app/client/src/utils/ValidationFactory.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { WidgetType } from "constants/WidgetConstants"; -import WidgetFactory from "./WidgetFactory"; -import { - VALIDATION_TYPES, - ValidationResponse, - ValidationType, - Validator, -} from "constants/WidgetValidation"; -import { WidgetProps } from "widgets/BaseWidget"; -import { DataTree } from "entities/DataTree/dataTreeFactory"; - -export const BASE_WIDGET_VALIDATION = { - isLoading: VALIDATION_TYPES.BOOLEAN, - isVisible: VALIDATION_TYPES.BOOLEAN, - isDisabled: VALIDATION_TYPES.BOOLEAN, -}; - -export type WidgetPropertyValidationType = Record< - string, - ValidationType | Validator ->; - -class ValidationFactory { - static validationMap: Map = new Map(); - - static registerValidator( - validationType: ValidationType, - validator: Validator, - ) { - this.validationMap.set(validationType, validator); - } - - static validateWidgetProperty( - widgetType: WidgetType, - property: string, - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse { - // TODO WIDGETFACTORY - const propertyValidationTypes = WidgetFactory.getWidgetPropertyValidationMap( - widgetType, - ); - const validationTypeOrValidator = propertyValidationTypes[property]; - let validator; - - if (typeof validationTypeOrValidator === "function") { - validator = validationTypeOrValidator; - } else { - validator = this.validationMap.get(validationTypeOrValidator); - } - if (validator) { - return validator(value, props, dataTree); - } else { - return { isValid: true, parsed: value }; - } - } -} - -export default ValidationFactory; diff --git a/app/client/src/utils/ValidationRegistry.ts b/app/client/src/utils/ValidationRegistry.ts deleted file mode 100644 index 875b68a2724..00000000000 --- a/app/client/src/utils/ValidationRegistry.ts +++ /dev/null @@ -1,13 +0,0 @@ -import ValidationFactory from "./ValidationFactory"; -import { VALIDATION_TYPES } from "constants/WidgetValidation"; -import { VALIDATORS } from "./Validators"; - -class ValidationRegistry { - static registerInternalValidators() { - Object.keys(VALIDATION_TYPES).forEach(type => { - ValidationFactory.registerValidator(type, VALIDATORS[type]); - }); - } -} - -export default ValidationRegistry; diff --git a/app/client/src/utils/Validators.ts b/app/client/src/utils/Validators.ts deleted file mode 100644 index d37806a99cf..00000000000 --- a/app/client/src/utils/Validators.ts +++ /dev/null @@ -1,523 +0,0 @@ -import _ from "lodash"; -import { - ISO_DATE_FORMAT, - VALIDATION_TYPES, - ValidationResponse, - ValidationType, - Validator, -} from "constants/WidgetValidation"; -import moment from "moment"; -import { WIDGET_TYPE_VALIDATION_ERROR } from "constants/messages"; -import { WidgetProps } from "widgets/BaseWidget"; -import { DataTree } from "entities/DataTree/dataTreeFactory"; - -export const VALIDATORS: Record = { - [VALIDATION_TYPES.TEXT]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (_.isUndefined(value) || value === null) { - return { - isValid: true, - parsed: value, - message: "", - }; - } - if (_.isObject(value)) { - return { - isValid: false, - parsed: JSON.stringify(value, null, 2), - message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, - }; - } - let isValid = _.isString(value); - if (!isValid) { - try { - parsed = _.toString(value); - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to string`); - console.error(e); - return { - isValid: false, - parsed: "", - message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.REGEX]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.TEXT]( - value, - props, - dataTree, - ); - - if (isValid) { - try { - new RegExp(parsed); - } catch (e) { - return { - isValid: false, - parsed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: regex`, - }; - } - } - - return { isValid, parsed, message }; - }, - [VALIDATION_TYPES.NUMBER]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (_.isUndefined(value)) { - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - let isValid = _.isNumber(value); - if (!isValid) { - try { - parsed = _.toNumber(value); - if (isNaN(parsed)) { - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to number`); - console.error(e); - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.BOOLEAN]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (_.isUndefined(value)) { - return { - isValid: false, - parsed: false, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, - }; - } - const isBoolean = _.isBoolean(value); - const isStringTrueFalse = value === "true" || value === "false"; - const isValid = isBoolean || isStringTrueFalse; - if (isStringTrueFalse) parsed = value !== "false"; - if (!isValid) { - return { - isValid: isValid, - parsed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.OBJECT]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (_.isUndefined(value)) { - return { - isValid: false, - parsed: {}, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, - }; - } - let isValid = _.isObject(value); - if (!isValid) { - try { - parsed = JSON.parse(value); - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to object`); - console.error(e); - return { - isValid: false, - parsed: {}, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.ARRAY]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - try { - if (_.isUndefined(value)) { - return { - isValid: false, - parsed: [], - transformed: undefined, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - if (_.isString(value)) { - parsed = JSON.parse(parsed as string); - } - if (!Array.isArray(parsed)) { - return { - isValid: false, - parsed: [], - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - return { isValid: true, parsed, transformed: parsed }; - } catch (e) { - console.error(e); - return { - isValid: false, - parsed: [], - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - }, - [VALIDATION_TYPES.TABS_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, - }; - } else if (!_.every(parsed, datum => _.isObject(datum))) { - return { - isValid: false, - parsed: [], - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.TABLE_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, transformed, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed: [], - transformed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, - }; - } - const isValidTableData = _.every(parsed, datum => { - return ( - _.isObject(datum) && - Object.keys(datum).filter(key => _.isString(key) && key.length === 0) - .length === 0 - ); - }); - if (!isValidTableData) { - return { - isValid: false, - parsed: [], - transformed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.CHART_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Chart Data`, - }; - } - let validationMessage = ""; - let index = 0; - const isValidChartData = _.every( - parsed, - (datum: { name: string; data: any }) => { - const validatedResponse: { - isValid: boolean; - parsed: Record; - message?: string; - } = VALIDATORS[VALIDATION_TYPES.ARRAY](datum.data, props, dataTree); - validationMessage = `${index}##${WIDGET_TYPE_VALIDATION_ERROR}: [{ "x": "val", "y": "val" }]`; - let isValidChart = validatedResponse.isValid; - if (validatedResponse.isValid) { - datum.data = validatedResponse.parsed; - isValidChart = _.every( - datum.data, - (chartPoint: { x: string; y: any }) => { - return ( - _.isObject(chartPoint) && - _.isString(chartPoint.x) && - !_.isUndefined(chartPoint.y) - ); - }, - ); - } - index++; - return isValidChart; - }, - ); - if (!isValidChartData) { - return { - isValid: false, - parsed: [], - transformed: parsed, - message: validationMessage, - }; - } - return { isValid, parsed, transformed: parsed }; - }, - [VALIDATION_TYPES.MARKERS]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, - }; - } else if (!_.every(parsed, datum => _.isObject(datum))) { - return { - isValid: false, - parsed: [], - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.OPTIONS_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, - }; - } - const isValidOption = (option: { label: any; value: any }) => - _.isString(option.label) && - _.isString(option.value) && - !_.isEmpty(option.label) && - !_.isEmpty(option.value); - - const hasOptions = _.every(parsed, (datum: { label: any; value: any }) => { - if (_.isObject(datum)) { - return isValidOption(datum); - } else { - return false; - } - }); - - const validOptions = parsed.filter(isValidOption); - const uniqValidOptions = _.uniqBy(validOptions, "value"); - - if (!hasOptions || uniqValidOptions.length !== validOptions.length) { - return { - isValid: false, - parsed: uniqValidOptions, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.DATE]: ( - dateString: string, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const today = moment() - .hour(0) - .minute(0) - .second(0) - .millisecond(0); - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; - - const todayDateString = today.format(dateFormat); - if (dateString === undefined) { - return { - isValid: false, - parsed: "", - message: - `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat - ? props.dateFormat - : "", - }; - } - const isValid = moment(dateString, dateFormat).isValid(); - const parsed = isValid ? dateString : todayDateString; - return { - isValid, - parsed, - message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date`, - }; - }, - [VALIDATION_TYPES.ACTION_SELECTOR]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - if (Array.isArray(value) && value.length) { - return { - isValid: true, - parsed: undefined, - transformed: "Function Call", - }; - } - /* - if (_.isString(value)) { - if (value.indexOf("navigateTo") !== -1) { - const pageNameOrUrl = modalGetter(value); - if (dataTree) { - if (isDynamicValue(pageNameOrUrl)) { - return { - isValid: true, - parsed: value, - }; - } - const isPage = - (dataTree.pageList as PageListPayload).findIndex( - page => page.pageName === pageNameOrUrl, - ) !== -1; - const isValidUrl = URL_REGEX.test(pageNameOrUrl); - if (!(isValidUrl || isPage)) { - return { - isValid: false, - parsed: value, - message: `${NAVIGATE_TO_VALIDATION_ERROR}`, - }; - } - } - } - } - */ - return { - isValid: false, - parsed: undefined, - transformed: "undefined", - message: "Not a function call", - }; - }, - [VALIDATION_TYPES.ARRAY_ACTION_SELECTOR]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - let isValidFinal = isValid; - let finalParsed = parsed.slice(); - if (isValid) { - finalParsed = parsed.map((value: any) => { - const { isValid, message } = VALIDATORS[ - VALIDATION_TYPES.ACTION_SELECTOR - ](value.dynamicTrigger, props, dataTree); - - isValidFinal = isValidFinal && isValid; - return { - ...value, - message: message, - isValid: isValid, - }; - }); - } - - return { - isValid: isValidFinal, - parsed: finalParsed, - message: message, - }; - }, - [VALIDATION_TYPES.SELECTED_TAB]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const tabs = - props.tabs && _.isString(props.tabs) - ? JSON.parse(props.tabs) - : props.tabs && Array.isArray(props.tabs) - ? props.tabs - : []; - const tabNames = tabs.map((i: { label: string; id: string }) => i.label); - const isValidTabName = tabNames.includes(value); - return { - isValid: isValidTabName, - parsed: value, - message: isValidTabName - ? "" - : `${WIDGET_TYPE_VALIDATION_ERROR}: Invalid tab name.`, - }; - }, -}; diff --git a/app/client/src/utils/WidgetFactory.tsx b/app/client/src/utils/WidgetFactory.tsx index a7d4d001fc4..e2491e46ec7 100644 --- a/app/client/src/utils/WidgetFactory.tsx +++ b/app/client/src/utils/WidgetFactory.tsx @@ -8,7 +8,7 @@ import { import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "./ValidationFactory"; +} from "./WidgetValidation"; import React from "react"; type WidgetDerivedPropertyType = any; @@ -142,8 +142,33 @@ class WidgetFactory { } return map; } + + static getWidgetTypeConfigMap(): WidgetTypeConfigMap { + const typeConfigMap: WidgetTypeConfigMap = {}; + WidgetFactory.getWidgetTypes().forEach(type => { + typeConfigMap[type] = { + validations: WidgetFactory.getWidgetPropertyValidationMap(type), + defaultProperties: WidgetFactory.getWidgetDefaultPropertiesMap(type), + derivedProperties: WidgetFactory.getWidgetDerivedPropertiesMap(type), + triggerProperties: WidgetFactory.getWidgetTriggerPropertiesMap(type), + metaProperties: WidgetFactory.getWidgetMetaPropertiesMap(type), + }; + }); + return typeConfigMap; + } } +export type WidgetTypeConfigMap = Record< + string, + { + validations: WidgetPropertyValidationType; + derivedProperties: WidgetDerivedPropertyType; + triggerProperties: TriggerPropertiesMap; + defaultProperties: Record; + metaProperties: Record; + } +>; + export interface WidgetCreationException { message: string; } diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx index ef15c269c82..09919a5f29b 100644 --- a/app/client/src/utils/WidgetRegistry.tsx +++ b/app/client/src/utils/WidgetRegistry.tsx @@ -80,6 +80,10 @@ import IconWidget, { } from "widgets/IconWidget"; import CanvasWidget, { ProfiledCanvasWidget } from "widgets/CanvasWidget"; +import SkeletonWidget, { + ProfiledSkeletonWidget, + SkeletonWidgetProps, +} from "../widgets/SkeletonWidget"; export default class WidgetBuilderRegistry { static registerWidgetBuilders() { WidgetFactory.registerWidgetBuilder( @@ -376,5 +380,19 @@ export default class WidgetBuilderRegistry { IconWidget.getDefaultPropertiesMap(), IconWidget.getMetaPropertiesMap(), ); + + WidgetFactory.registerWidgetBuilder( + WidgetTypes.SKELETON_WIDGET, + { + buildWidget(widgetProps: SkeletonWidgetProps): JSX.Element { + return ; + }, + }, + SkeletonWidget.getPropertyValidationMap(), + SkeletonWidget.getDerivedPropertiesMap(), + SkeletonWidget.getTriggerPropertyMap(), + SkeletonWidget.getDefaultPropertiesMap(), + SkeletonWidget.getMetaPropertiesMap(), + ); } } diff --git a/app/client/src/utils/WidgetValidation.ts b/app/client/src/utils/WidgetValidation.ts new file mode 100644 index 00000000000..a45d0839819 --- /dev/null +++ b/app/client/src/utils/WidgetValidation.ts @@ -0,0 +1,16 @@ +import { + VALIDATION_TYPES, + ValidationType, + Validator, +} from "constants/WidgetValidation"; + +export const BASE_WIDGET_VALIDATION = { + isLoading: VALIDATION_TYPES.BOOLEAN, + isVisible: VALIDATION_TYPES.BOOLEAN, + isDisabled: VALIDATION_TYPES.BOOLEAN, +}; + +export type WidgetPropertyValidationType = Record< + string, + ValidationType | Validator +>; diff --git a/app/client/src/utils/autocomplete/TernServer.test.ts b/app/client/src/utils/autocomplete/TernServer.test.ts index c391290c648..08f13c88662 100644 --- a/app/client/src/utils/autocomplete/TernServer.test.ts +++ b/app/client/src/utils/autocomplete/TernServer.test.ts @@ -1,6 +1,5 @@ import TernServer from "./TernServer"; import { MockCodemirrorEditor } from "../../../test/__mocks__/CodeMirrorEditorMock"; -jest.mock("jsExecution/RealmExecutor"); describe("Tern server", () => { it("Check whether the correct value is being sent to tern", () => { @@ -96,7 +95,7 @@ describe("Tern server", () => { }); }); - it(`Check whether the position is evaluated correctly for placing the selected + it(`Check whether the position is evaluated correctly for placing the selected autocomplete value`, () => { const ternServer = new TernServer({}); diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index 06492b064ac..0b0e42a164a 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -28,7 +28,7 @@ import ErrorBoundary from "components/editorComponents/ErrorBoundry"; import { BASE_WIDGET_VALIDATION, WidgetPropertyValidationType, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { DerivedPropertiesMap, TriggerPropertiesMap, @@ -325,6 +325,23 @@ export interface WidgetPositionProps extends WidgetRowCols { detachFromLayout?: boolean; } +export const WIDGET_STATIC_PROPS = { + leftColumn: true, + rightColumn: true, + topRow: true, + bottomRow: true, + minHeight: true, + parentColumnSpace: true, + parentRowSpace: true, + children: true, + type: true, + widgetId: true, + widgetName: true, + parentId: true, + renderMode: true, + detachFromLayout: true, +}; + export interface WidgetDisplayProps { //TODO(abhinav): Some of these props are mandatory isVisible?: boolean; diff --git a/app/client/src/widgets/ButtonWidget.tsx b/app/client/src/widgets/ButtonWidget.tsx index 98caf8b0353..4d03ec79e40 100644 --- a/app/client/src/widgets/ButtonWidget.tsx +++ b/app/client/src/widgets/ButtonWidget.tsx @@ -8,7 +8,7 @@ import { EventType } from "constants/ActionConstants"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; import * as Sentry from "@sentry/react"; diff --git a/app/client/src/widgets/ChartWidget.tsx b/app/client/src/widgets/ChartWidget.tsx index 5d4159ef109..b1f430ff2d1 100644 --- a/app/client/src/widgets/ChartWidget.tsx +++ b/app/client/src/widgets/ChartWidget.tsx @@ -1,7 +1,7 @@ import React, { lazy, Suspense } from "react"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; -import { WidgetPropertyValidationType } from "utils/ValidationFactory"; +import { WidgetPropertyValidationType } from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import Skeleton from "components/utils/Skeleton"; import * as Sentry from "@sentry/react"; diff --git a/app/client/src/widgets/CheckboxWidget.tsx b/app/client/src/widgets/CheckboxWidget.tsx index f31369e553a..8add0de23f6 100644 --- a/app/client/src/widgets/CheckboxWidget.tsx +++ b/app/client/src/widgets/CheckboxWidget.tsx @@ -7,7 +7,7 @@ import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { TriggerPropertiesMap, DerivedPropertiesMap, diff --git a/app/client/src/widgets/DatePickerWidget.tsx b/app/client/src/widgets/DatePickerWidget.tsx index d76833285ae..08b580674c6 100644 --- a/app/client/src/widgets/DatePickerWidget.tsx +++ b/app/client/src/widgets/DatePickerWidget.tsx @@ -6,7 +6,7 @@ import DatePickerComponent from "components/designSystems/blueprint/DatePickerCo import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { DerivedPropertiesMap, diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx index 88589e13ba5..c50828fd675 100644 --- a/app/client/src/widgets/DropdownWidget.tsx +++ b/app/client/src/widgets/DropdownWidget.tsx @@ -7,11 +7,9 @@ import _ from "lodash"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; -import { VALIDATORS } from "utils/Validators"; -import { DataTree } from "entities/DataTree/dataTreeFactory"; import { Intent as BlueprintIntent } from "@blueprintjs/core"; import * as Sentry from "@sentry/react"; import withMeta, { WithMeta } from "./MetaHOC"; @@ -28,42 +26,7 @@ class DropdownWidget extends BaseWidget { // onOptionChange: VALIDATION_TYPES.ACTION_SELECTOR, selectedOptionValueArr: VALIDATION_TYPES.ARRAY, selectedOptionValues: VALIDATION_TYPES.ARRAY, - defaultOptionValue: ( - value: string | string[], - props: WidgetProps, - dataTree?: DataTree, - ) => { - let values = value; - - if (props) { - if (props.selectionType === "SINGLE_SELECT") { - return VALIDATORS[VALIDATION_TYPES.TEXT](value, props, dataTree); - } else if (props.selectionType === "MULTI_SELECT") { - if (typeof value === "string") { - try { - values = JSON.parse(value); - if (!Array.isArray(values)) { - throw new Error(); - } - } catch { - values = value.length ? value.split(",") : []; - if (values.length > 0) { - values = values.map(value => value.trim()); - } - } - } - } - } - - if (Array.isArray(values)) { - values = _.uniq(values); - } - - return { - isValid: true, - parsed: values, - }; - }, + defaultOptionValue: VALIDATION_TYPES.DEFAULT_OPTION_VALUE, }; } diff --git a/app/client/src/widgets/FilepickerWidget.tsx b/app/client/src/widgets/FilepickerWidget.tsx index c61ad7c78d4..ced1ee4091a 100644 --- a/app/client/src/widgets/FilepickerWidget.tsx +++ b/app/client/src/widgets/FilepickerWidget.tsx @@ -10,7 +10,7 @@ import OneDrive from "@uppy/onedrive"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { EventType, ExecutionResult } from "constants/ActionConstants"; import { diff --git a/app/client/src/widgets/FormButtonWidget.tsx b/app/client/src/widgets/FormButtonWidget.tsx index 4ed30b4c7d9..fecb3a16256 100644 --- a/app/client/src/widgets/FormButtonWidget.tsx +++ b/app/client/src/widgets/FormButtonWidget.tsx @@ -8,7 +8,7 @@ import { EventType, ExecutionResult } from "constants/ActionConstants"; import { BASE_WIDGET_VALIDATION, WidgetPropertyValidationType, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; import * as Sentry from "@sentry/react"; diff --git a/app/client/src/widgets/FormWidget.tsx b/app/client/src/widgets/FormWidget.tsx index 76fdda810d5..16a91e8590b 100644 --- a/app/client/src/widgets/FormWidget.tsx +++ b/app/client/src/widgets/FormWidget.tsx @@ -11,8 +11,12 @@ import withMeta from "./MetaHOC"; class FormWidget extends ContainerWidget { checkInvalidChildren = (children: WidgetProps[]): boolean => { return _.some(children, child => { - if ("children" in child) return this.checkInvalidChildren(child.children); - if ("isValid" in child) return !child.isValid; + if ("children" in child) { + return this.checkInvalidChildren(child.children); + } + if ("isValid" in child) { + return !child.isValid; + } return false; }); }; diff --git a/app/client/src/widgets/ImageWidget.test.tsx b/app/client/src/widgets/ImageWidget.test.tsx index de4307ad21d..d58fef38f89 100644 --- a/app/client/src/widgets/ImageWidget.test.tsx +++ b/app/client/src/widgets/ImageWidget.test.tsx @@ -1,7 +1,6 @@ // import React from "react"; // import { render, fireEvent } from "@testing-library/react"; // import ImageWidget, { ImageWidgetProps } from "./ImageWidget"; -// import RealmExecutor from "../jsExecution/RealmExecutor"; // import { useDrag } from "react-dnd"; // import { Provider } from "react-redux"; @@ -10,8 +9,6 @@ // import "@testing-library/jest-dom"; -// jest.mock("jsExecution/RealmExecutor"); - // jest.mock("react-dnd", () => ({ // useDrag: jest.fn().mockReturnValue([{ isDragging: false }, jest.fn()]), // })); diff --git a/app/client/src/widgets/ImageWidget.tsx b/app/client/src/widgets/ImageWidget.tsx index edaf2207824..a8b2c3f5c57 100644 --- a/app/client/src/widgets/ImageWidget.tsx +++ b/app/client/src/widgets/ImageWidget.tsx @@ -5,7 +5,7 @@ import ImageComponent from "components/designSystems/appsmith/ImageComponent"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import * as Sentry from "@sentry/react"; import { EventType } from "constants/ActionConstants"; diff --git a/app/client/src/widgets/InputWidget.tsx b/app/client/src/widgets/InputWidget.tsx index c86fb3d8d9c..87594cc5dea 100644 --- a/app/client/src/widgets/InputWidget.tsx +++ b/app/client/src/widgets/InputWidget.tsx @@ -8,7 +8,7 @@ import { EventType } from "constants/ActionConstants"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { FIELD_REQUIRED_ERROR } from "constants/messages"; import { diff --git a/app/client/src/widgets/MapWidget.tsx b/app/client/src/widgets/MapWidget.tsx index 3612a81e1ff..6bb0f5b36a2 100644 --- a/app/client/src/widgets/MapWidget.tsx +++ b/app/client/src/widgets/MapWidget.tsx @@ -2,7 +2,7 @@ import React from "react"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; import MapComponent from "components/designSystems/appsmith/MapComponent"; -import { WidgetPropertyValidationType } from "utils/ValidationFactory"; +import { WidgetPropertyValidationType } from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { EventType } from "constants/ActionConstants"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; diff --git a/app/client/src/widgets/MetaHOC.tsx b/app/client/src/widgets/MetaHOC.tsx index 3fe0c60b8db..d370cefa799 100644 --- a/app/client/src/widgets/MetaHOC.tsx +++ b/app/client/src/widgets/MetaHOC.tsx @@ -2,13 +2,15 @@ import React from "react"; import BaseWidget, { WidgetProps } from "./BaseWidget"; import _ from "lodash"; import { EditorContext } from "../components/editorComponents/EditorContextProvider"; -import { clearPropertyCache } from "../utils/DynamicBindingUtils"; +import { clearEvalPropertyCache } from "sagas/evaluationsSaga"; import { ExecuteActionPayload } from "../constants/ActionConstants"; type DebouncedExecuteActionPayload = Omit< ExecuteActionPayload, "dynamicString" -> & { dynamicString?: string }; +> & { + dynamicString?: string; +}; export interface WithMeta { updateWidgetMetaProperty: ( @@ -87,7 +89,7 @@ const withMeta = (WrappedWidget: typeof BaseWidget) => { [...this.updatedProperties.keys()].forEach(propertyName => { if (updateWidgetMetaProperty) { const propertyValue = this.state[propertyName]; - clearPropertyCache(`${widgetName}.${propertyName}`); + clearEvalPropertyCache(`${widgetName}.${propertyName}`); updateWidgetMetaProperty(widgetId, propertyName, propertyValue); this.updatedProperties.delete(propertyName); } diff --git a/app/client/src/widgets/RadioGroupWidget.tsx b/app/client/src/widgets/RadioGroupWidget.tsx index 76673ace9d0..267c1644a45 100644 --- a/app/client/src/widgets/RadioGroupWidget.tsx +++ b/app/client/src/widgets/RadioGroupWidget.tsx @@ -6,7 +6,7 @@ import { EventType } from "constants/ActionConstants"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; import * as Sentry from "@sentry/react"; diff --git a/app/client/src/widgets/RichTextEditorWidget.tsx b/app/client/src/widgets/RichTextEditorWidget.tsx index c3816c7ac6c..03089337b4f 100644 --- a/app/client/src/widgets/RichTextEditorWidget.tsx +++ b/app/client/src/widgets/RichTextEditorWidget.tsx @@ -2,7 +2,7 @@ import React, { lazy, Suspense } from "react"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; import { EventType } from "constants/ActionConstants"; -import { WidgetPropertyValidationType } from "utils/ValidationFactory"; +import { WidgetPropertyValidationType } from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { TriggerPropertiesMap, diff --git a/app/client/src/widgets/SkeletonWidget.tsx b/app/client/src/widgets/SkeletonWidget.tsx new file mode 100644 index 00000000000..d40b1c38ea9 --- /dev/null +++ b/app/client/src/widgets/SkeletonWidget.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; +import { WidgetType } from "constants/WidgetConstants"; +import * as Sentry from "@sentry/react"; +import styled from "styled-components"; + +const SkeletonWrapper = styled.div` + height: 100%; + width: 100%; +`; + +class SkeletonWidget extends BaseWidget { + getPageView() { + return ; + } + + getWidgetType(): WidgetType { + return "SKELETON_WIDGET"; + } +} + +export interface SkeletonWidgetProps extends WidgetProps { + isLoading: boolean; +} + +export default SkeletonWidget; +export const ProfiledSkeletonWidget = Sentry.withProfiler(SkeletonWidget); diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index 419529fd5e5..c376eba2bff 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -15,7 +15,7 @@ import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { BASE_WIDGET_VALIDATION, WidgetPropertyValidationType, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; import Skeleton from "components/utils/Skeleton"; diff --git a/app/client/src/widgets/TabsWidget.tsx b/app/client/src/widgets/TabsWidget.tsx index fdb803e5617..56af247ff28 100644 --- a/app/client/src/widgets/TabsWidget.tsx +++ b/app/client/src/widgets/TabsWidget.tsx @@ -3,7 +3,7 @@ import TabsComponent from "components/designSystems/appsmith/TabsComponent"; import { WidgetType, WidgetTypes } from "constants/WidgetConstants"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import WidgetFactory, { TriggerPropertiesMap } from "utils/WidgetFactory"; -import { WidgetPropertyValidationType } from "utils/ValidationFactory"; +import { WidgetPropertyValidationType } from "utils/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import _ from "lodash"; import { EventType } from "constants/ActionConstants"; diff --git a/app/client/src/widgets/TextWidget.tsx b/app/client/src/widgets/TextWidget.tsx index 46afe1121f5..748a9c6317a 100644 --- a/app/client/src/widgets/TextWidget.tsx +++ b/app/client/src/widgets/TextWidget.tsx @@ -6,7 +6,7 @@ import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { DerivedPropertiesMap } from "utils/WidgetFactory"; import * as Sentry from "@sentry/react"; diff --git a/app/client/src/widgets/VideoWidget.tsx b/app/client/src/widgets/VideoWidget.tsx index d0fae3e6d60..09ef27893c3 100644 --- a/app/client/src/widgets/VideoWidget.tsx +++ b/app/client/src/widgets/VideoWidget.tsx @@ -6,7 +6,7 @@ import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { WidgetPropertyValidationType, BASE_WIDGET_VALIDATION, -} from "utils/ValidationFactory"; +} from "utils/WidgetValidation"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; import Skeleton from "components/utils/Skeleton"; import * as Sentry from "@sentry/react"; diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts new file mode 100644 index 00000000000..9e200fdb76b --- /dev/null +++ b/app/client/src/workers/evaluation.worker.ts @@ -0,0 +1,1604 @@ +/* eslint no-restricted-globals: 0 */ +import { + ISO_DATE_FORMAT, + VALIDATION_TYPES, + ValidationResponse, + ValidationType, + Validator, +} from "../constants/WidgetValidation"; +import { + ActionDescription, + DataTree, + DataTreeAction, + DataTreeEntity, + DataTreeWidget, + ENTITY_TYPE, +} from "../entities/DataTree/dataTreeFactory"; +import equal from "fast-deep-equal/es6"; +import * as log from "loglevel"; +import _, { + every, + isBoolean, + isNumber, + isObject, + isString, + isUndefined, + toNumber, + toString, +} from "lodash"; +import toposort from "toposort"; +import { DATA_BIND_REGEX } from "../constants/BindingsConstants"; +import unescapeJS from "unescape-js"; +import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; +import { WidgetType } from "../constants/WidgetConstants"; +import { WidgetProps } from "../widgets/BaseWidget"; +import { WIDGET_TYPE_VALIDATION_ERROR } from "../constants/messages"; +import moment from "moment"; +import { + EVAL_WORKER_ACTIONS, + EvalError, + EvalErrorTypes, + extraLibraries, +} from "../utils/DynamicBindingUtils"; + +const ctx: Worker = self as any; + +let ERRORS: EvalError[] = []; +let WIDGET_TYPE_CONFIG_MAP: WidgetTypeConfigMap = {}; + +ctx.addEventListener("message", e => { + const { action, ...rest } = e.data; + + switch (action as EVAL_WORKER_ACTIONS) { + case EVAL_WORKER_ACTIONS.EVAL_TREE: { + const { widgetTypeConfigMap, dataTree } = rest; + WIDGET_TYPE_CONFIG_MAP = widgetTypeConfigMap; + const response = getEvaluatedDataTree(dataTree); + // We need to clean it to remove any possible functions inside the tree. + // If functions exist, it will crash the web worker + const cleanDataTree = JSON.stringify(response); + ctx.postMessage({ dataTree: cleanDataTree, errors: ERRORS }); + ERRORS = []; + break; + } + case EVAL_WORKER_ACTIONS.EVAL_SINGLE: { + const { binding, dataTree } = rest; + const withFunctions = addFunctions(dataTree); + const value = getDynamicValue(binding, withFunctions, false); + ctx.postMessage({ value, errors: ERRORS }); + ERRORS = []; + break; + } + case EVAL_WORKER_ACTIONS.EVAL_TRIGGER: { + const { dynamicTrigger, callbackData, dataTree } = rest; + const evalTree = getEvaluatedDataTree(dataTree); + const withFunctions = addFunctions(evalTree); + const triggers = getDynamicValue( + dynamicTrigger, + withFunctions, + true, + callbackData, + ); + ctx.postMessage({ triggers, errors: ERRORS }); + ERRORS = []; + break; + } + case EVAL_WORKER_ACTIONS.CLEAR_CACHE: { + clearCaches(); + ctx.postMessage(true); + break; + } + case EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE: { + const { propertyPath } = rest; + clearPropertyCache(propertyPath); + ctx.postMessage(true); + break; + } + } +}); + +let dependencyTreeCache: any = {}; +let cachedDataTreeString = ""; + +function getEvaluatedDataTree(dataTree: DataTree): DataTree { + const totalStart = performance.now(); + // Add functions to the tre + const withFunctions = addFunctions(dataTree); + // Create Dependencies DAG + const createDepsStart = performance.now(); + const dataTreeString = JSON.stringify(dataTree); + // Stringify before doing a fast equals because the data tree has functions and fast equal will always treat those as changed values + // Better solve will be to prune functions + if (!equal(dataTreeString, cachedDataTreeString)) { + cachedDataTreeString = dataTreeString; + dependencyTreeCache = createDependencyTree(withFunctions); + } + const createDepsEnd = performance.now(); + const { + dependencyMap, + sortedDependencies, + dependencyTree, + } = dependencyTreeCache; + + // Evaluate Tree + const evaluatedTreeStart = performance.now(); + const evaluatedTree = dependencySortedEvaluateDataTree( + dataTree, + dependencyMap, + sortedDependencies, + ); + const evaluatedTreeEnd = performance.now(); + + // Set Loading Widgets + const loadingTreeStart = performance.now(); + const treeWithLoading = setTreeLoading(evaluatedTree, dependencyTree); + const loadingTreeEnd = performance.now(); + + // Validate Widgets + const validated = getValidatedTree(treeWithLoading); + + const withoutFunctions = removeFunctionsFromDataTree(validated); + + // End counting total time + const endStart = performance.now(); + + // Log time taken and count + const timeTaken = { + total: (endStart - totalStart).toFixed(2), + createDeps: (createDepsEnd - createDepsStart).toFixed(2), + evaluate: (evaluatedTreeEnd - evaluatedTreeStart).toFixed(2), + loading: (loadingTreeEnd - loadingTreeStart).toFixed(2), + }; + log.debug("data tree evaluated"); + log.debug(timeTaken); + // dataTreeCache = validated; + return withoutFunctions; +} + +const addFunctions = (dataTree: DataTree): DataTree => { + dataTree.actionPaths = []; + Object.keys(dataTree).forEach(entityName => { + const entity = dataTree[entityName]; + if ( + entity && + "ENTITY_TYPE" in entity && + entity.ENTITY_TYPE === ENTITY_TYPE.ACTION + ) { + const runFunction = function( + this: DataTreeAction, + onSuccess: string, + onError: string, + params = "", + ) { + return { + type: "RUN_ACTION", + payload: { + actionId: this.actionId, + onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "", + onError: onError ? `{{${onError.toString()}}}` : "", + params, + }, + }; + }; + _.set(dataTree, `${entityName}.run`, runFunction); + dataTree.actionPaths && dataTree.actionPaths.push(`${entityName}.run`); + } + }); + dataTree.navigateTo = function( + pageNameOrUrl: string, + params: Record, + ) { + return { + type: "NAVIGATE_TO", + payload: { pageNameOrUrl, params }, + }; + }; + dataTree.actionPaths.push("navigateTo"); + + dataTree.showAlert = function(message: string, style: string) { + return { + type: "SHOW_ALERT", + payload: { message, style }, + }; + }; + dataTree.actionPaths.push("showAlert"); + + dataTree.showModal = function(modalName: string) { + return { + type: "SHOW_MODAL_BY_NAME", + payload: { modalName }, + }; + }; + dataTree.actionPaths.push("showModal"); + + dataTree.closeModal = function(modalName: string) { + return { + type: "CLOSE_MODAL", + payload: { modalName }, + }; + }; + dataTree.actionPaths.push("closeModal"); + + dataTree.storeValue = function(key: string, value: string) { + return { + type: "STORE_VALUE", + payload: { key, value }, + }; + }; + dataTree.actionPaths.push("storeValue"); + + dataTree.download = function(data: string, name: string, type: string) { + return { + type: "DOWNLOAD", + payload: { data, name, type }, + }; + }; + dataTree.actionPaths.push("download"); + return dataTree; +}; + +const removeFunctionsFromDataTree = (dataTree: DataTree) => { + dataTree.actionPaths?.forEach(functionPath => { + _.set(dataTree, functionPath, {}); + }); + delete dataTree.actionPaths; + return dataTree; +}; + +// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing +// Check issue https://github.com/appsmithorg/appsmith/issues/719 +const removeFunctions = (value: any) => { + if (_.isFunction(value)) { + return "Function call"; + } else if (_.isObject(value) && _.some(value, _.isFunction)) { + return JSON.parse(JSON.stringify(value)); + } else { + return value; + } +}; + +type DynamicDependencyMap = Record>; +const createDependencyTree = ( + dataTree: DataTree, +): { + sortedDependencies: Array; + dependencyTree: Array<[string, string]>; + dependencyMap: DynamicDependencyMap; +} => { + const dependencyMap: DynamicDependencyMap = {}; + const allKeys = getAllPaths(dataTree); + Object.keys(dataTree).forEach(entityKey => { + const entity = dataTree[entityKey]; + if (entity && "ENTITY_TYPE" in entity) { + if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { + // Set default property dependency + const defaultProperties = + WIDGET_TYPE_CONFIG_MAP[entity.type].defaultProperties; + Object.keys(defaultProperties).forEach(property => { + dependencyMap[`${entityKey}.${property}`] = [ + `${entityKey}.${defaultProperties[property]}`, + ]; + }); + if (entity.dynamicBindings) { + Object.keys(entity.dynamicBindings).forEach(propertyName => { + // using unescape to remove new lines from bindings which interfere with our regex extraction + const unevalPropValue = _.get(entity, propertyName); + const { jsSnippets } = getDynamicBindings(unevalPropValue); + const existingDeps = + dependencyMap[`${entityKey}.${propertyName}`] || []; + dependencyMap[`${entityKey}.${propertyName}`] = existingDeps.concat( + jsSnippets.filter(jsSnippet => !!jsSnippet), + ); + }); + } + if (entity.dynamicTriggers) { + Object.keys(entity.dynamicTriggers).forEach(prop => { + dependencyMap[`${entityKey}.${prop}`] = []; + }); + } + } + if (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION) { + if (entity.dynamicBindingPathList.length) { + entity.dynamicBindingPathList.forEach(prop => { + // using unescape to remove new lines from bindings which interfere with our regex extraction + const unevalPropValue = _.get(entity, prop.key); + const { jsSnippets } = getDynamicBindings(unevalPropValue); + const existingDeps = + dependencyMap[`${entityKey}.${prop.key}`] || []; + dependencyMap[`${entityKey}.${prop.key}`] = existingDeps.concat( + jsSnippets.filter(jsSnippet => !!jsSnippet), + ); + }); + } + } + } + }); + Object.keys(dependencyMap).forEach(key => { + dependencyMap[key] = _.flatten( + dependencyMap[key].map(path => calculateSubDependencies(path, allKeys)), + ); + }); + const dependencyTree: Array<[string, string]> = []; + Object.keys(dependencyMap).forEach((key: string) => { + if (dependencyMap[key].length) { + dependencyMap[key].forEach(dep => dependencyTree.push([key, dep])); + } else { + // Set no dependency + dependencyTree.push([key, ""]); + } + }); + + try { + // sort dependencies and remove empty dependencies + const sortedDependencies = toposort(dependencyTree) + .reverse() + .filter(d => !!d); + + return { sortedDependencies, dependencyMap, dependencyTree }; + } catch (e) { + ERRORS.push({ + type: EvalErrorTypes.DEPENDENCY_ERROR, + message: e.message, + }); + return { sortedDependencies: [], dependencyMap: {}, dependencyTree: [] }; + } +}; + +const calculateSubDependencies = ( + path: string, + all: Record, +): Array => { + const subDeps: Array = []; + const identifiers = path.match(/[a-zA-Z_$][a-zA-Z_$0-9.]*/g) || [path]; + identifiers.forEach((identifier: string) => { + if (all.hasOwnProperty(identifier)) { + subDeps.push(identifier); + } else { + const subIdentifiers = + identifier.match(/[a-zA-Z_$][a-zA-Z_$0-9]*/g) || []; + let current = ""; + for (let i = 0; i < subIdentifiers.length; i++) { + const key = `${current}${current ? "." : ""}${subIdentifiers[i]}`; + if (key in all) { + current = key; + } else { + break; + } + } + if (current && current.includes(".")) subDeps.push(current); + } + }); + return _.uniq(subDeps); +}; + +const setTreeLoading = ( + dataTree: DataTree, + dependencyMap: Array<[string, string]>, +) => { + const widgets: string[] = []; + const isLoadingActions: string[] = []; + + // Fetch all actions that are in loading state + Object.keys(dataTree).forEach(e => { + const entity = dataTree[e]; + if (entity && "ENTITY_TYPE" in entity) { + if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { + widgets.push(e); + } else if ( + entity.ENTITY_TYPE === ENTITY_TYPE.ACTION && + entity.isLoading + ) { + isLoadingActions.push(e); + } + } + }); + + // get all widget dependencies of those actions + isLoadingActions + .reduce( + (allEntities: string[], curr) => + allEntities.concat(getEntityDependencies(dependencyMap, curr, widgets)), + [], + ) + // set loading to true for those widgets + .forEach(w => { + const entity = dataTree[w] as DataTreeWidget; + entity.isLoading = true; + }); + return dataTree; +}; + +const getEntityDependencies = ( + dependencyMap: Array<[string, string]>, + entity: string, + entities: string[], +): Array => { + const entityDeps: Record = dependencyMap + .map(d => [d[1].split(".")[0], d[0].split(".")[0]]) + .filter(d => d[0] !== d[1]) + .reduce((deps: Record, dep) => { + const key: string = dep[0]; + const value: string = dep[1]; + return { + ...deps, + [key]: deps[key] ? deps[key].concat(value) : [value], + }; + }, {}); + + if (entity in entityDeps) { + const recFind = ( + keys: Array, + deps: Record, + ): Array => { + let allDeps: string[] = []; + keys + .filter(k => entities.includes(k)) + .forEach(e => { + allDeps = allDeps.concat([e]); + if (e in deps) { + allDeps = allDeps.concat([...recFind(deps[e], deps)]); + } + }); + return allDeps; + }; + return recFind(entityDeps[entity], entityDeps); + } + return []; +}; + +function dependencySortedEvaluateDataTree( + dataTree: DataTree, + dependencyMap: DynamicDependencyMap, + sortedDependencies: Array, +): DataTree { + const tree = _.cloneDeep(dataTree); + try { + return sortedDependencies.reduce( + (currentTree: DataTree, propertyPath: string) => { + const entityName = propertyPath.split(".")[0]; + const entity: DataTreeEntity = currentTree[entityName]; + const unEvalPropertyValue = _.get(currentTree as any, propertyPath); + let evalPropertyValue; + const propertyDependencies = dependencyMap[propertyPath]; + const currentDependencyValues = getCurrentDependencyValues( + propertyDependencies, + currentTree, + propertyPath, + ); + const cachedDependencyValues = dependencyCache.get(propertyPath); + const requiresEval = isDynamicValue(unEvalPropertyValue); + if (requiresEval) { + try { + evalPropertyValue = evaluateDynamicProperty( + propertyPath, + currentTree, + unEvalPropertyValue, + currentDependencyValues, + cachedDependencyValues, + ); + } catch (e) { + ERRORS.push({ + type: EvalErrorTypes.EVAL_PROPERTY_ERROR, + message: e.message, + context: { + propertyPath, + }, + }); + evalPropertyValue = undefined; + } + } else { + evalPropertyValue = unEvalPropertyValue; + // If we have stored any previous dependency cache, clear it + // since it is no longer a binding + if (cachedDependencyValues && cachedDependencyValues.length) { + dependencyCache.set(propertyPath, []); + } + } + if (isWidget(entity)) { + const widgetEntity: DataTreeWidget = entity as DataTreeWidget; + const propertyName = propertyPath.split(".")[1]; + if (propertyName) { + let parsedValue = validateAndParseWidgetProperty( + propertyPath, + widgetEntity, + currentTree, + evalPropertyValue, + unEvalPropertyValue, + currentDependencyValues, + cachedDependencyValues, + ); + const defaultPropertyMap = + WIDGET_TYPE_CONFIG_MAP[widgetEntity.type].defaultProperties; + const hasDefaultProperty = propertyName in defaultPropertyMap; + if (hasDefaultProperty) { + const defaultProperty = defaultPropertyMap[propertyName]; + parsedValue = overwriteDefaultDependentProps( + defaultProperty, + parsedValue, + propertyPath, + widgetEntity, + ); + } + return _.set(currentTree, propertyPath, parsedValue); + } + return _.set(currentTree, propertyPath, evalPropertyValue); + } else { + return _.set(currentTree, propertyPath, evalPropertyValue); + } + }, + tree, + ); + } catch (e) { + ERRORS.push({ + type: EvalErrorTypes.EVAL_TREE_ERROR, + message: e.message, + }); + return tree; + } +} + +const overwriteDefaultDependentProps = ( + defaultProperty: string, + propertyValue: any, + propertyPath: string, + entity: DataTreeWidget, +) => { + const defaultPropertyCache = getParsedValueCache( + `${entity.widgetName}.${defaultProperty}`, + ); + const propertyCache = getParsedValueCache(propertyPath); + if ( + propertyValue === undefined || + propertyCache.version < defaultPropertyCache.version + ) { + return defaultPropertyCache.value; + } + return propertyValue; +}; + +const getValidatedTree = (tree: any) => { + return Object.keys(tree).reduce((tree, entityKey: string) => { + const entity = tree[entityKey]; + if (entity && entity.type) { + const parsedEntity = { ...entity }; + Object.keys(entity).forEach((property: string) => { + const hasEvaluatedValue = _.has( + parsedEntity, + `evaluatedValues.${property}`, + ); + const hasValidation = _.has(parsedEntity, `invalidProps.${property}`); + const isSpecialField = [ + "dynamicBindings", + "dynamicTriggers", + "dynamicProperties", + "evaluatedValues", + "invalidProps", + "validationMessages", + ].includes(property); + const isDynamicField = + _.has(parsedEntity, `dynamicBindings.${property}`) || + _.has(parsedEntity, `dynamicTriggers.${property}`); + + if ( + !isSpecialField && + !isDynamicField && + (!hasValidation || !hasEvaluatedValue) + ) { + const value = entity[property]; + // Pass it through parse + const { + parsed, + isValid, + message, + transformed, + } = validateWidgetProperty( + entity.type, + property, + value, + entity, + tree, + ); + parsedEntity[property] = parsed; + if (!hasEvaluatedValue) { + const evaluatedValue = isValid + ? parsed + : _.isUndefined(transformed) + ? value + : transformed; + const safeEvaluatedValue = removeFunctions(evaluatedValue); + _.set( + parsedEntity, + `evaluatedValues.${property}`, + safeEvaluatedValue, + ); + } + + const hasValidation = _.has(parsedEntity, `invalidProps.${property}`); + if (!hasValidation && !isValid) { + _.set(parsedEntity, `invalidProps.${property}`, true); + _.set(parsedEntity, `validationMessages.${property}`, message); + } + } + }); + return { ...tree, [entityKey]: parsedEntity }; + } + return tree; + }, tree); +}; + +const getAllPaths = ( + tree: Record, + prefix = "", +): Record => { + return Object.keys(tree).reduce((res: Record, el): Record< + string, + true + > => { + if (Array.isArray(tree[el])) { + const key = `${prefix}${el}`; + return { ...res, [key]: true }; + } else if (typeof tree[el] === "object" && tree[el] !== null) { + const key = `${prefix}${el}`; + return { ...res, [key]: true, ...getAllPaths(tree[el], `${key}.`) }; + } else { + const key = `${prefix}${el}`; + return { ...res, [key]: true }; + } + }, {}); +}; + +const getDynamicBindings = ( + dynamicString: string, +): { stringSegments: string[]; jsSnippets: string[] } => { + // Protect against bad string parse + if (!dynamicString || !_.isString(dynamicString)) { + return { stringSegments: [], jsSnippets: [] }; + } + const sanitisedString = dynamicString.trim(); + // Get the {{binding}} bound values + const stringSegments = getDynamicStringSegments(sanitisedString); + // Get the "binding" path values + const paths = stringSegments.map(segment => { + const length = segment.length; + const matches = isDynamicValue(segment); + if (matches) { + return segment.substring(2, length - 2); + } + return ""; + }); + return { stringSegments: stringSegments, jsSnippets: paths }; +}; + +//{{}}{{}}} +function getDynamicStringSegments(dynamicString: string): string[] { + let stringSegments = []; + const indexOfDoubleParanStart = dynamicString.indexOf("{{"); + if (indexOfDoubleParanStart === -1) { + return [dynamicString]; + } + //{{}}{{}}} + const firstString = dynamicString.substring(0, indexOfDoubleParanStart); + firstString && stringSegments.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) { + stringSegments.push(rest.substring(0, i + 1)); + rest = rest.substring(i + 1, rest.length); + if (rest) { + stringSegments = stringSegments.concat( + getDynamicStringSegments(rest), + ); + break; + } + } + } + } + if (sum !== 0 && dynamicString !== "") { + return [dynamicString]; + } + return stringSegments; +} + +// referencing DATA_BIND_REGEX fails for the value "{{Table1.tableData[Table1.selectedRowIndex]}}" if you run it multiple times and don't recreate +const isDynamicValue = (value: string): boolean => DATA_BIND_REGEX.test(value); + +function getCurrentDependencyValues( + propertyDependencies: Array, + currentTree: DataTree, + currentPropertyPath: string, +): Array { + return propertyDependencies + ? propertyDependencies + .map((path: string) => { + //*** Remove current path from data tree because cached value contains evaluated version while this contains unevaluated version */ + const cleanDataTree = _.omit(currentTree, [currentPropertyPath]); + return _.get(cleanDataTree, path); + }) + .filter((data: any) => { + return data !== undefined; + }) + : []; +} + +const dynamicPropValueCache: Map< + string, + { + unEvaluated: any; + evaluated: any; + } +> = new Map(); + +const parsedValueCache: Map< + string, + { + value: any; + version: number; + } +> = new Map(); + +const getDynamicPropValueCache = (propertyPath: string) => + dynamicPropValueCache.get(propertyPath); + +const getParsedValueCache = (propertyPath: string) => + parsedValueCache.get(propertyPath) || { + value: undefined, + version: 0, + }; + +const clearPropertyCache = (propertyPath: string) => + parsedValueCache.delete(propertyPath); + +const dependencyCache: Map = new Map(); + +function isWidget(entity: DataTreeEntity): boolean { + return "ENTITY_TYPE" in entity && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET; +} + +function validateAndParseWidgetProperty( + propertyPath: string, + widget: DataTreeWidget, + currentTree: DataTree, + evalPropertyValue: any, + unEvalPropertyValue: string, + currentDependencyValues: Array, + cachedDependencyValues?: Array, +): any { + const propertyName = propertyPath.split(".")[1]; + let valueToValidate = evalPropertyValue; + if (widget.dynamicTriggers && propertyName in widget.dynamicTriggers) { + const { triggers } = getDynamicValue( + unEvalPropertyValue, + currentTree, + true, + undefined, + ); + valueToValidate = triggers; + } + const { parsed, isValid, message, transformed } = validateWidgetProperty( + widget.type, + propertyName, + valueToValidate, + widget, + currentTree, + ); + const evaluatedValue = isValid + ? parsed + : _.isUndefined(transformed) + ? evalPropertyValue + : transformed; + const safeEvaluatedValue = removeFunctions(evaluatedValue); + _.set(widget, `evaluatedValues.${propertyName}`, safeEvaluatedValue); + if (!isValid) { + _.set(widget, `invalidProps.${propertyName}`, true); + _.set(widget, `validationMessages.${propertyName}`, message); + } + + if (widget.dynamicTriggers && propertyName in widget.dynamicTriggers) { + return unEvalPropertyValue; + } else { + const parsedCache = getParsedValueCache(propertyPath); + if ( + !equal(parsedCache.value, parsed) || + (cachedDependencyValues !== undefined && + !equal(currentDependencyValues, cachedDependencyValues)) + ) { + parsedValueCache.set(propertyPath, { + value: parsed, + version: Date.now(), + }); + } + return parsed; + } +} + +function evaluateDynamicProperty( + propertyPath: string, + currentTree: DataTree, + unEvalPropertyValue: any, + currentDependencyValues: Array, + cachedDependencyValues?: Array, +): any { + const cacheObj = getDynamicPropValueCache(propertyPath); + const isCacheHit = + cacheObj && + equal(cacheObj.unEvaluated, unEvalPropertyValue) && + cachedDependencyValues !== undefined && + equal(currentDependencyValues, cachedDependencyValues); + if (isCacheHit && cacheObj) { + return cacheObj.evaluated; + } else { + log.debug("eval " + propertyPath); + const dynamicResult = getDynamicValue( + unEvalPropertyValue, + currentTree, + false, + ); + dynamicPropValueCache.set(propertyPath, { + evaluated: dynamicResult, + unEvaluated: unEvalPropertyValue, + }); + dependencyCache.set(propertyPath, currentDependencyValues); + return dynamicResult; + } +} + +type EvalResult = { + result: any; + triggers?: ActionDescription[]; +}; +// Paths are expected to have "{name}.{path}" signature +// Also returns any action triggers found after evaluating value +const evaluateDynamicBoundValue = ( + data: DataTree, + path: string, + callbackData?: any, +): EvalResult => { + try { + const unescapedJS = unescapeJS(path).replace(/(\r\n|\n|\r)/gm, ""); + return evaluate(unescapedJS, data, callbackData); + } catch (e) { + ERRORS.push({ + type: EvalErrorTypes.UNESCAPE_STRING_ERROR, + message: e.message, + context: { + path, + }, + }); + return { result: undefined, triggers: [] }; + } +}; + +const evaluate = ( + js: string, + data: DataTree, + callbackData: any, +): EvalResult => { + const scriptToEvaluate = ` + function closedFunction () { + const result = ${js}; + return { result, triggers: self.triggers } + } + closedFunction() + `; + const scriptWithCallback = ` + function callback (script) { + const userFunction = script; + const result = userFunction(CALLBACK_DATA); + return { result, triggers: self.triggers }; + } + callback(${js}); + `; + const script = callbackData ? scriptWithCallback : scriptToEvaluate; + try { + const { result, triggers } = (function() { + /**** Setting the eval context ****/ + const GLOBAL_DATA: Record = {}; + ///// Adding callback data + GLOBAL_DATA.CALLBACK_DATA = callbackData; + ///// Adding Data tree + Object.keys(data).forEach(datum => { + GLOBAL_DATA[datum] = data[datum]; + }); + ///// Fixing action paths and capturing their execution response + if (data.actionPaths) { + GLOBAL_DATA.triggers = []; + const pusher = function( + this: DataTree, + action: any, + ...payload: any[] + ) { + const actionPayload = action(...payload); + GLOBAL_DATA.triggers.push(actionPayload); + }; + GLOBAL_DATA.actionPaths.forEach((path: string) => { + const action = _.get(GLOBAL_DATA, path); + const entity = _.get(GLOBAL_DATA, path.split(".")[0]); + if (action) { + _.set(GLOBAL_DATA, path, pusher.bind(data, action.bind(entity))); + } + }); + } + + // Set it to self + Object.keys(GLOBAL_DATA).forEach(key => { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + self[key] = GLOBAL_DATA[key]; + }); + + ///// Adding extra libraries separately + extraLibraries.forEach(library => { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + self[library.accessor] = library.lib; + }); + + const evalResult = eval(script); + + // Remove it from self + // This is needed so that next eval can have a clean sheet + Object.keys(GLOBAL_DATA).forEach(key => { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + delete self[key]; + }); + + return evalResult; + })(); + return { result, triggers }; + } catch (e) { + ERRORS.push({ + type: EvalErrorTypes.EVAL_ERROR, + message: e.message, + context: { + binding: js, + }, + }); + return { result: undefined, triggers: [] }; + } +}; + +// For creating a final value where bindings could be in a template format +const createDynamicValueString = ( + binding: string, + subBindings: string[], + subValues: string[], +): string => { + // Replace the string with the data tree values + let finalValue = binding; + subBindings.forEach((b, i) => { + let value = subValues[i]; + if (Array.isArray(value) || _.isObject(value)) { + value = JSON.stringify(value); + } + try { + if (JSON.parse(value)) { + value = value.replace(/\\([\s\S])|(")/g, "\\$1$2"); + } + } catch (e) { + // do nothing + } + finalValue = finalValue.replace(b, value); + }); + return finalValue; +}; + +const getDynamicValue = ( + dynamicBinding: string, + data: DataTree, + returnTriggers: boolean, + callBackData?: any, +) => { + // Get the {{binding}} bound values + const { stringSegments, jsSnippets } = getDynamicBindings(dynamicBinding); + if (returnTriggers) { + const result = evaluateDynamicBoundValue(data, jsSnippets[0], callBackData); + return result.triggers; + } + if (stringSegments.length) { + // Get the Data Tree value of those "binding "paths + const values = jsSnippets.map((jsSnippet, index) => { + if (jsSnippet) { + const result = evaluateDynamicBoundValue(data, jsSnippet, callBackData); + return result.result; + } else { + return stringSegments[index]; + } + }); + + // if it is just one binding, no need to create template string + if (stringSegments.length === 1) return values[0]; + // else return a string template with bindings + return createDynamicValueString(dynamicBinding, stringSegments, values); + } + return undefined; +}; + +const validateWidgetProperty = ( + widgetType: WidgetType, + property: string, + value: any, + props: WidgetProps, + dataTree?: DataTree, +) => { + const propertyValidationTypes = + WIDGET_TYPE_CONFIG_MAP[widgetType].validations; + const validationTypeOrValidator = propertyValidationTypes[property]; + let validator; + + if (typeof validationTypeOrValidator === "function") { + validator = validationTypeOrValidator; + } else { + validator = VALIDATORS[validationTypeOrValidator]; + } + if (validator) { + return validator(value, props, dataTree); + } else { + return { isValid: true, parsed: value }; + } +}; + +const clearCaches = () => { + dynamicPropValueCache.clear(); + dependencyCache.clear(); + parsedValueCache.clear(); +}; + +const VALIDATORS: Record = { + [VALIDATION_TYPES.TEXT]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + let parsed = value; + if (isUndefined(value) || value === null) { + return { + isValid: true, + parsed: value, + message: "", + }; + } + if (isObject(value)) { + return { + isValid: false, + parsed: JSON.stringify(value, null, 2), + message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, + }; + } + let isValid = isString(value); + if (!isValid) { + try { + parsed = toString(value); + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to string`); + console.error(e); + return { + isValid: false, + parsed: "", + message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.REGEX]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.TEXT]( + value, + props, + dataTree, + ); + + if (isValid) { + try { + new RegExp(parsed); + } catch (e) { + return { + isValid: false, + parsed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: regex`, + }; + } + } + + return { isValid, parsed, message }; + }, + [VALIDATION_TYPES.NUMBER]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + let isValid = isNumber(value); + if (!isValid) { + try { + parsed = toNumber(value); + if (isNaN(parsed)) { + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to number`); + console.error(e); + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.BOOLEAN]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: false, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, + }; + } + const isABoolean = isBoolean(value); + const isStringTrueFalse = value === "true" || value === "false"; + const isValid = isABoolean || isStringTrueFalse; + if (isStringTrueFalse) parsed = value !== "false"; + if (!isValid) { + return { + isValid: isValid, + parsed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.OBJECT]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: {}, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, + }; + } + let isValid = isObject(value); + if (!isValid) { + try { + parsed = JSON.parse(value); + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to object`); + console.error(e); + return { + isValid: false, + parsed: {}, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.ARRAY]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + let parsed = value; + try { + if (isUndefined(value)) { + return { + isValid: false, + parsed: [], + transformed: undefined, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + if (isString(value)) { + parsed = JSON.parse(parsed as string); + } + if (!Array.isArray(parsed)) { + return { + isValid: false, + parsed: [], + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + return { isValid: true, parsed, transformed: parsed }; + } catch (e) { + console.error(e); + return { + isValid: false, + parsed: [], + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + }, + [VALIDATION_TYPES.TABS_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, + }; + } else if (!every(parsed, datum => isObject(datum))) { + return { + isValid: false, + parsed: [], + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.TABLE_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, transformed, parsed } = VALIDATORS.ARRAY( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed: [], + transformed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, + }; + } + const isValidTableData = every(parsed, datum => { + return ( + isObject(datum) && + Object.keys(datum).filter(key => isString(key) && key.length === 0) + .length === 0 + ); + }); + if (!isValidTableData) { + return { + isValid: false, + parsed: [], + transformed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.CHART_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Chart Data`, + }; + } + let validationMessage = ""; + let index = 0; + const isValidChartData = every( + parsed, + (datum: { name: string; data: any }) => { + const validatedResponse: { + isValid: boolean; + parsed: Array; + message?: string; + } = VALIDATORS[VALIDATION_TYPES.ARRAY](datum.data, props, dataTree); + validationMessage = `${index}##${WIDGET_TYPE_VALIDATION_ERROR}: [{ "x": "val", "y": "val" }]`; + let isValidChart = validatedResponse.isValid; + if (validatedResponse.isValid) { + datum.data = validatedResponse.parsed; + isValidChart = every( + datum.data, + (chartPoint: { x: string; y: any }) => { + return ( + isObject(chartPoint) && + isString(chartPoint.x) && + !isUndefined(chartPoint.y) + ); + }, + ); + } + index++; + return isValidChart; + }, + ); + if (!isValidChartData) { + return { + isValid: false, + parsed: [], + transformed: parsed, + message: validationMessage, + }; + } + return { isValid, parsed, transformed: parsed }; + }, + [VALIDATION_TYPES.MARKERS]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, + }; + } else if (!every(parsed, datum => isObject(datum))) { + return { + isValid: false, + parsed: [], + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.OPTIONS_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, + }; + } + + const isValidOption = (option: { label: any; value: any }) => + _.isString(option.label) && + _.isString(option.value) && + !_.isEmpty(option.label) && + !_.isEmpty(option.value); + + const hasOptions = every(parsed, (datum: { label: any; value: any }) => { + if (isObject(datum)) { + return isValidOption(datum); + } else { + return false; + } + }); + const validOptions = parsed.filter(isValidOption); + const uniqValidOptions = _.uniqBy(validOptions, "value"); + + if (!hasOptions || uniqValidOptions.length !== validOptions.length) { + return { + isValid: false, + parsed: uniqValidOptions, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.DATE]: ( + dateString: string, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const today = moment() + .hour(0) + .minute(0) + .second(0) + .millisecond(0); + const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + + const todayDateString = today.format(dateFormat); + if (dateString === undefined) { + return { + isValid: false, + parsed: "", + message: + `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat + ? props.dateFormat + : "", + }; + } + const isValid = moment(dateString, dateFormat).isValid(); + const parsed = isValid ? dateString : todayDateString; + return { + isValid, + parsed, + message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date`, + }; + }, + [VALIDATION_TYPES.ACTION_SELECTOR]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + if (Array.isArray(value) && value.length) { + return { + isValid: true, + parsed: undefined, + transformed: "Function Call", + }; + } + /* + if (_.isString(value)) { + if (value.indexOf("navigateTo") !== -1) { + const pageNameOrUrl = modalGetter(value); + if (dataTree) { + if (isDynamicValue(pageNameOrUrl)) { + return { + isValid: true, + parsed: value, + }; + } + const isPage = + (dataTree.pageList as PageListPayload).findIndex( + page => page.pageName === pageNameOrUrl, + ) !== -1; + const isValidUrl = URL_REGEX.test(pageNameOrUrl); + if (!(isValidUrl || isPage)) { + return { + isValid: false, + parsed: value, + message: `${NAVIGATE_TO_VALIDATION_ERROR}`, + }; + } + } + } + } + */ + return { + isValid: false, + parsed: undefined, + transformed: "undefined", + message: "Not a function call", + }; + }, + [VALIDATION_TYPES.ARRAY_ACTION_SELECTOR]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + let isValidFinal = isValid; + let finalParsed = parsed.slice(); + if (isValid) { + finalParsed = parsed.map((value: any) => { + const { isValid, message } = VALIDATORS[ + VALIDATION_TYPES.ACTION_SELECTOR + ](value.dynamicTrigger, props, dataTree); + + isValidFinal = isValidFinal && isValid; + return { + ...value, + message: message, + isValid: isValid, + }; + }); + } + + return { + isValid: isValidFinal, + parsed: finalParsed, + message: message, + }; + }, + [VALIDATION_TYPES.SELECTED_TAB]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const tabs = + props.tabs && isString(props.tabs) + ? JSON.parse(props.tabs) + : props.tabs && Array.isArray(props.tabs) + ? props.tabs + : []; + const tabNames = tabs.map((i: { label: string; id: string }) => i.label); + const isValidTabName = tabNames.includes(value); + return { + isValid: isValidTabName, + parsed: value, + message: isValidTabName + ? "" + : `${WIDGET_TYPE_VALIDATION_ERROR}: Invalid tab name.`, + }; + }, + [VALIDATION_TYPES.DEFAULT_OPTION_VALUE]: ( + value: string | string[], + props: WidgetProps, + dataTree?: DataTree, + ) => { + let values = value; + + if (props) { + if (props.selectionType === "SINGLE_SELECT") { + return VALIDATORS[VALIDATION_TYPES.TEXT](value, props, dataTree); + } else if (props.selectionType === "MULTI_SELECT") { + if (typeof value === "string") { + try { + values = JSON.parse(value); + if (!Array.isArray(values)) { + throw new Error(); + } + } catch { + values = value.length ? value.split(",") : []; + if (values.length > 0) { + values = values.map(value => value.trim()); + } + } + } + } + } + + if (Array.isArray(values)) { + values = _.uniq(values); + } + + return { + isValid: true, + parsed: values, + }; + }, +}; diff --git a/app/client/test/__mocks__/RealmExecutorMock.ts b/app/client/test/__mocks__/RealmExecutorMock.ts index 96ec32303d1..a152614261a 100644 --- a/app/client/test/__mocks__/RealmExecutorMock.ts +++ b/app/client/test/__mocks__/RealmExecutorMock.ts @@ -11,9 +11,3 @@ export const mockExecute = jest.fn().mockImplementation((src, data) => { }); export const mockRegisterLibrary = jest.fn(); - -// jest.mock("jsExecution/RealmExecutor", () => { -// jest.fn().mockImplementation(() => { -// return { execute: mockExecute, registerLibrary: mockRegisterLibrary }; -// }); -// }); diff --git a/app/client/typings/worker-loader/index.d.ts b/app/client/typings/worker-loader/index.d.ts new file mode 100644 index 00000000000..6a93b95693d --- /dev/null +++ b/app/client/typings/worker-loader/index.d.ts @@ -0,0 +1,7 @@ +declare module "worker-loader!*" { + class WebpackWorker extends Worker { + constructor(); + } + + export default WebpackWorker; +} diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 67d6f464db0..506f423104e 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -1836,10 +1836,10 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jest/types@^26.3.0": - version "26.3.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" - integrity sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ== +"@jest/types@^26.5.2": + version "26.5.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.5.2.tgz#44c24f30c8ee6c7f492ead9ec3f3c62a5289756d" + integrity sha512-QDs5d0gYiyetI8q+2xWdkixVQMklReZr4ltw7GFDtb4fuJIBCE6mzj2LnitGqCuAlLap6wPyb8fpoHgwZz5fdg== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -1969,9 +1969,9 @@ uuid "^3.3.2" "@optimizely/optimizely-sdk@^4.0.0": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@optimizely/optimizely-sdk/-/optimizely-sdk-4.3.3.tgz#91db4072f8e439d997370ad48c25106610f2e4a3" - integrity sha512-tz6GyJMM4TUpLTsoyO9ZKNB5/UswtKSF4jLlBG1N6hWVi+bCJUa25M4Ok6DA0izZ2k0Y2xmUQDVmV4p23Li5Gw== + version "4.3.4" + resolved "https://registry.yarnpkg.com/@optimizely/optimizely-sdk/-/optimizely-sdk-4.3.4.tgz#b323b91dc8af9656dde8bcf696801bd71443e202" + integrity sha512-DqaEg9YwiwnfDjaDmbST2cu0/7W/yQJqQ+tBwIEwh/HqiSgs8oQJX7sNG2Ql2fFwlIzG7APkIx/oxwbXpp8LPg== dependencies: "@optimizely/js-sdk-datafile-manager" "^0.8.0" "@optimizely/js-sdk-event-processor" "^0.6.0" @@ -2074,14 +2074,14 @@ resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.1.0.tgz#b84b4a91cd938a688d36245b7a7db6fbc476a499" integrity sha512-b2iE8kjjzzUo2WZ0xuE2N77kfnTds7ClrDxcz3Atz7h2XrNVoAPUoT75i7CY0st5x++70V91Y+c6RpBX9MX7Jg== -"@sentry/browser@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.25.0.tgz#4e3d2132ba1f2e2b26f73c49cbb6977ee9c9fea9" - integrity sha512-QDVUbUuTu58xCdId0eUO4YzpvrPdoUw1ryVy/Yep9Es/HD0fiSyO1Js0eQVkV/EdXtyo2pomc1Bpy7dbn2EJ2w== +"@sentry/browser@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.26.0.tgz#e90a197fb94c5f26c8e05d6a539c118f33c7d598" + integrity sha512-52kNVpy10Zd3gJRGFkhnOQvr80WJg7+XBqjMOE0//Akh4PfvEK3IqmAjVqysz6aHdruwTTivKF4ZoAxL/pA7Rg== dependencies: - "@sentry/core" "5.25.0" - "@sentry/types" "5.25.0" - "@sentry/utils" "5.25.0" + "@sentry/core" "5.26.0" + "@sentry/types" "5.26.0" + "@sentry/utils" "5.26.0" tslib "^1.9.3" "@sentry/cli@^1.58.0": @@ -2095,69 +2095,69 @@ progress "^2.0.3" proxy-from-env "^1.1.0" -"@sentry/core@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.25.0.tgz#525ad37f9e8a95603768e3b74b437d5235a51578" - integrity sha512-hY6Zmo7t/RV+oZuvXHP6nyAj/QnZr2jW0e7EbL5YKMV8q0vlnjcE0LgqFXme726OJemoLk67z+sQOJic/Ztehg== +"@sentry/core@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.26.0.tgz#9b5fe4de8a869d733ebcc77f5ec9c619f8717a51" + integrity sha512-Ubrw7K52orTVsaxpz8Su40FPXugKipoQC+zPrXcH+JIMB+o18kutF81Ae4WzuUqLfP7YB91eAlRrP608zw0EXA== dependencies: - "@sentry/hub" "5.25.0" - "@sentry/minimal" "5.25.0" - "@sentry/types" "5.25.0" - "@sentry/utils" "5.25.0" + "@sentry/hub" "5.26.0" + "@sentry/minimal" "5.26.0" + "@sentry/types" "5.26.0" + "@sentry/utils" "5.26.0" tslib "^1.9.3" -"@sentry/hub@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.25.0.tgz#6932535604cafaee1ac7f361b0e7c2ce8f7e7bc3" - integrity sha512-kOlOiJV8wMX50lYpzMlOXBoH7MNG0Ho4RTusdZnXZBaASq5/ljngDJkLr6uylNjceZQP21wzipCQajsJMYB7EQ== +"@sentry/hub@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.26.0.tgz#b2bbd8128cd5915f2ee59cbc29fff30272d74ec5" + integrity sha512-lAYeWvvhGYS6eQ5d0VEojw0juxGc3v4aAu8VLvMKWcZ1jXD13Bhc46u9Nvf4qAY6BAQsJDQcpEZLpzJu1bk1Qw== dependencies: - "@sentry/types" "5.25.0" - "@sentry/utils" "5.25.0" + "@sentry/types" "5.26.0" + "@sentry/utils" "5.26.0" tslib "^1.9.3" -"@sentry/minimal@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.25.0.tgz#447b5406b45c8c436c461abea4474d6a849ed975" - integrity sha512-9JFKuW7U+1vPO86k3+XRtJyooiVZsVOsFFO4GulBzepi3a0ckNyPgyjUY1saLH+cEHx18hu8fGgajvI8ANUF2g== +"@sentry/minimal@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.26.0.tgz#851dea3644153ed3ac4837fa8ed5661d94e7a313" + integrity sha512-mdFo3FYaI1W3KEd8EHATYx8mDOZIxeoUhcBLlH7Iej6rKvdM7p8GoECrmHPU1l6sCCPtBuz66QT5YeXc7WILsA== dependencies: - "@sentry/hub" "5.25.0" - "@sentry/types" "5.25.0" + "@sentry/hub" "5.26.0" + "@sentry/types" "5.26.0" tslib "^1.9.3" "@sentry/react@^5.24.2": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.25.0.tgz#269f54db9d6f92410bee07117f8d8e03b219e068" - integrity sha512-lZwiFj+BQtmaj+Do9hcRSJcdrTisSGq2521/Xm9qGPbhsRW8uTHMJjkDgMHriYxxqXYOQrY9FisJwvkPpkroow== - dependencies: - "@sentry/browser" "5.25.0" - "@sentry/minimal" "5.25.0" - "@sentry/types" "5.25.0" - "@sentry/utils" "5.25.0" + version "5.26.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.26.0.tgz#0462430757ac0ab0c10de803f39fb41d5ced1caa" + integrity sha512-oC1wwfhckV8HHJTs4Zot5JIwEftcltPuC8cOedenDor5SKKbMeNufKw0ZgW82j9DSZMjh053LKkIZmO7zYf8eQ== + dependencies: + "@sentry/browser" "5.26.0" + "@sentry/minimal" "5.26.0" + "@sentry/types" "5.26.0" + "@sentry/utils" "5.26.0" hoist-non-react-statics "^3.3.2" tslib "^1.9.3" "@sentry/tracing@^5.24.2": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.25.0.tgz#1cfbcf085a7a3b679f417058d09590298ddaa255" - integrity sha512-KcyHEGFpqSDubHrdWT/vF2hKkjw/ts6NpJ6tPDjBXUNz98BHdAyMKtLOFTCeJFply7/s5fyiAYu44M+M6IG3Bw== - dependencies: - "@sentry/hub" "5.25.0" - "@sentry/minimal" "5.25.0" - "@sentry/types" "5.25.0" - "@sentry/utils" "5.25.0" + version "5.26.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.26.0.tgz#33ee0426da14836e54e7b9a8838e4d7d0cb14b70" + integrity sha512-N9qWGmKrFJYKFTZBe8zVT3Qiju0+9bbNJuyun69T+fqP3PCDh+aRlRiP+OKTJyeCZjNG5HIvIlU8wTVUDoYfjQ== + dependencies: + "@sentry/hub" "5.26.0" + "@sentry/minimal" "5.26.0" + "@sentry/types" "5.26.0" + "@sentry/utils" "5.26.0" tslib "^1.9.3" -"@sentry/types@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.25.0.tgz#3bcf95e118d655d3f4e8bfa5f0be2e1fe4ea5307" - integrity sha512-8M4PREbcar+15wrtEqcwfcU33SS+2wBSIOd/NrJPXJPTYxi49VypCN1mZBDyWkaK+I+AuQwI3XlRPCfsId3D1A== +"@sentry/types@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.26.0.tgz#b0cbacb0b24cd86620fb296b46cf7277bb004a3e" + integrity sha512-ugpa1ePOhK55pjsyutAsa2tiJVQEyGYCaOXzaheg/3+EvhMdoW+owiZ8wupfvPhtZFIU3+FPOVz0d5k9K5d1rw== -"@sentry/utils@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.25.0.tgz#b132034be66d7381d30879d2a9e09216fed28342" - integrity sha512-Hz5spdIkMSRH5NR1YFOp5qbsY5Ud2lKhEQWlqxcVThMG5YNUc10aYv5ijL19v0YkrC2rqPjCRm7GrVtzOc7bXQ== +"@sentry/utils@5.26.0": + version "5.26.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.26.0.tgz#09a3d01d91747f38f796cafeb24f8fd86e4fa05f" + integrity sha512-F2gnHIAWbjiowcAgxz3VpKxY/NQ39NTujEd/NPnRTWlRynLFg3bAV+UvZFXljhYJeN3b/zRlScNDcpCWTrtZGw== dependencies: - "@sentry/types" "5.25.0" + "@sentry/types" "5.26.0" tslib "^1.9.3" "@sentry/webpack-plugin@^1.12.1": @@ -2871,9 +2871,9 @@ loader-utils "^1.2.3" "@testing-library/dom@^7.24.2": - version "7.24.3" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.3.tgz#dae3071463cf28dc7755b43d9cf2202e34cbb85d" - integrity sha512-6eW9fUhEbR423FZvoHRwbWm9RUUByLWGayYFNVvqTnQLYvsNpBS4uEuKH9aqr3trhxFwGVneJUonehL3B1sHJw== + version "7.26.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.26.0.tgz#da4d052dc426a4ccc916303369c6e7552126f680" + integrity sha512-fyKFrBbS1IigaE3FV21LyeC7kSGF84lqTlSYdKmGaHuK2eYQ/bXVPM5vAa2wx/AU1iPD6oQHsxy2QQ17q9AMCg== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.10.3" @@ -2881,6 +2881,7 @@ aria-query "^4.2.2" chalk "^4.1.0" dom-accessibility-api "^0.5.1" + lz-string "^1.4.4" pretty-format "^26.4.2" "@testing-library/jest-dom@^5.11.4": @@ -3016,9 +3017,9 @@ "@types/node" "*" "@types/googlemaps@^3.39.6": - version "3.39.14" - resolved "https://registry.yarnpkg.com/@types/googlemaps/-/googlemaps-3.39.14.tgz#971a7b474e7dcf6af1019fdccb91a69f74c5b262" - integrity sha512-MB8zwqarykWxCayoWcQAeKWpFTgKlCz7Z+PpFeEimAe4keQ1o1Q3Y/5r5Td52gd164QmTUHGxGlKkmUEVrdbHA== + version "3.40.0" + resolved "https://registry.yarnpkg.com/@types/googlemaps/-/googlemaps-3.40.0.tgz#786743648ab464fbeaa4cfe15230a7297273b309" + integrity sha512-KcAYVKjd5fL0Ur9G4xNL5YG/Bp5HFfdd8s/7j97eFcTyTpp7vIRcf8mtRBAIOM3QNgN2iJhSEecWTG2x8D+bnQ== "@types/hast@^2.0.0": version "2.3.1" @@ -3097,15 +3098,15 @@ dependencies: jest-diff "^24.3.0" -"@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5": +"@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== "@types/lodash@^4.14.120": - version "4.14.161" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" - integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA== + version "4.14.162" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.162.tgz#65d78c397e0d883f44afbf1f7ba9867022411470" + integrity sha512-alvcho1kRUnnD1Gcl4J+hK0eencvzq9rmzvFPRmP5rPHx9VVsJj6bKLTATPVf9ktgv4ujzh7T+XWKp+jhuODig== "@types/mdast@^3.0.0": version "3.0.3" @@ -3134,14 +3135,14 @@ "@types/node" "*" "@types/node@*": - version "14.11.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.5.tgz#fecad41c041cae7f2404ad4b2d0742fdb628b305" - integrity sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ== + version "14.11.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.8.tgz#fe2012f2355e4ce08bca44aeb3abbb21cf88d33f" + integrity sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw== "@types/node@^10.12.18": - version "10.17.37" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.37.tgz#40d03db879993799c3819e298b003f055e8ecafe" - integrity sha512-4c38N7p9k9yqdcANh/WExTahkBgOTmggCyrTvVcbE8ByqO3g8evt/407v/I4X/gdfUkIyZBSQh/Rc3tvuwlVGw== + version "10.17.39" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.39.tgz#ce1122758d0608de8303667cebf171f44192629b" + integrity sha512-dJLCxrpQmgyxYGcl0Ae9MTsQgI22qHHcGFj/8VKu7McJA5zQpnuGjoksnxbo1JxSjW/Nahnl13W8MYZf01CZHA== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -3266,9 +3267,9 @@ "@types/react" "*" "@types/react-select@^3.0.5": - version "3.0.21" - resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-3.0.21.tgz#089e1caa98dd653347cf4057b087957d26580d67" - integrity sha512-jVtukxaARMQCJC74ikGzpxrzxDFPkcGEwLsFIsc/bAn2rcBUlJ0WgP71ShUc/Q0nnk4ClZn2jBm2tbh6HtxB3A== + version "3.0.22" + resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-3.0.22.tgz#b88306365e99fa86809a5c0ce0f1b4e8d0b626bf" + integrity sha512-fqgmC979JPr/6476Pau6QnmI9zVV664R7Q92Ld1rgTn+umtUXT5X3+PO/x6O4imCZnh7XCqZcouabWAlAQJNpQ== dependencies: "@types/react" "*" "@types/react-dom" "*" @@ -3310,9 +3311,9 @@ "@types/react" "*" "@types/react@*", "@types/react@^16.8.2": - version "16.9.51" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.51.tgz#f8aa51ffa9996f1387f63686696d9b59713d2b60" - integrity sha512-lQa12IyO+DMlnSZ3+AGHRUiUcpK47aakMMoBG8f7HGxJT8Yfe+WE128HIXaHOHVPReAW0oDS3KAI0JI2DDe1PQ== + version "16.9.52" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.52.tgz#c46c72d1a1d8d9d666f4dd2066c0e22600ccfde1" + integrity sha512-EHRjmnxiNivwhGdMh9sz1Yw9AUxTSZFxKqdBWAAzyZx3sufWwx6ogqHYh/WB1m/I4ZpjkoZLExF5QTy2ekVi/Q== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -3372,9 +3373,9 @@ integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== "@types/styled-components@^5.1.3": - version "5.1.3" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.3.tgz#6fab3d9c8f7d9a15cbb89d379d850c985002f363" - integrity sha512-HGpirof3WOhiX17lb61Q/tpgqn48jxO8EfZkdJ8ueYqwLbK2AHQe/G08DasdA2IdKnmwOIP1s9X2bopxKXgjRw== + version "5.1.4" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.4.tgz#11f167dbde268635c66adc89b5a5db2e69d75384" + integrity sha512-78f5Zuy0v/LTQNOYfpH+CINHpchzMMmAt9amY2YNtSgsk1TmlKm8L2Wijss/mtTrsUAVTm2CdGB8VOM65vA8xg== dependencies: "@types/hoist-non-react-statics" "*" "@types/react" "*" @@ -3475,44 +3476,35 @@ "@types/yargs-parser" "*" "@types/yargs@^15.0.0": - version "15.0.7" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.7.tgz#dad50a7a234a35ef9460737a56024287a3de1d2b" - integrity sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA== + version "15.0.8" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.8.tgz#7644904cad7427eb704331ea9bf1ee5499b82e23" + integrity sha512-b0BYzFUzBpOhPjpl1wtAHU994jBeKF4TKVlT7ssFv44T617XNcPdRoG4AzHLVshLzlrF7i3lTelH7UbuNYV58Q== dependencies: "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^2.10.0": - version "2.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.19.0.tgz#bf743448a4633e4b52bee0c40148ba072ab3adbd" + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" + integrity sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ== dependencies: - "@typescript-eslint/experimental-utils" "2.19.0" - eslint-utils "^1.4.3" + "@typescript-eslint/experimental-utils" "2.34.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" "@typescript-eslint/eslint-plugin@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.4.0.tgz#0321684dd2b902c89128405cf0385e9fe8561934" - integrity sha512-RVt5wU9H/2H+N/ZrCasTXdGbUTkbf7Hfi9eLiA8vPQkzUJ/bLDCC3CsoZioPrNcnoyN8r0gT153dC++A4hKBQQ== + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.4.1.tgz#b8acea0373bd2a388ac47df44652f00bf8b368f5" + integrity sha512-O+8Utz8pb4OmcA+Nfi5THQnQpHSD2sDUNw9AxNHpuYOo326HZTtG8gsfT+EAYuVrFNaLyNb2QnUNkmTRDskuRA== dependencies: - "@typescript-eslint/experimental-utils" "4.4.0" - "@typescript-eslint/scope-manager" "4.4.0" + "@typescript-eslint/experimental-utils" "4.4.1" + "@typescript-eslint/scope-manager" "4.4.1" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.19.0": - version "2.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.19.0.tgz#d5ca732f22c009e515ba09fcceb5f2127d841568" - integrity sha512-zwpg6zEOPbhB3+GaQfufzlMUOO6GXCNZq6skk+b2ZkZAIoBhVoanWK255BS1g5x9bMwHpLhX0Rpn5Fc3NdCZdg== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.19.0" - eslint-scope "^5.0.0" - "@typescript-eslint/experimental-utils@2.34.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" @@ -3523,15 +3515,15 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.4.0.tgz#62a05d3f543b8fc5dec4982830618ea4d030e1a9" - integrity sha512-01+OtK/oWeSJTjQcyzDztfLF1YjvKpLFo+JZmurK/qjSRcyObpIecJ4rckDoRCSh5Etw+jKfdSzVEHevh9gJ1w== +"@typescript-eslint/experimental-utils@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.4.1.tgz#40613b9757fa0170de3e0043254dbb077cafac0c" + integrity sha512-Nt4EVlb1mqExW9cWhpV6pd1a3DkUbX9DeyYsdoeziKOpIJ04S2KMVDO+SEidsXRH/XHDpbzXykKcMTLdTXH6cQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.4.0" - "@typescript-eslint/types" "4.4.0" - "@typescript-eslint/typescript-estree" "4.4.0" + "@typescript-eslint/scope-manager" "4.4.1" + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/typescript-estree" "4.4.1" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -3546,40 +3538,27 @@ eslint-visitor-keys "^1.1.0" "@typescript-eslint/parser@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.4.0.tgz#65974db9a75f23b036f17b37e959b5f99b659ec0" - integrity sha512-yc14iEItCxoGb7W4Nx30FlTyGpU9r+j+n1LUK/exlq2eJeFxczrz/xFRZUk2f6yzWfK+pr1DOTyQnmDkcC4TnA== + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.4.1.tgz#25fde9c080611f303f2f33cedb145d2c59915b80" + integrity sha512-S0fuX5lDku28Au9REYUsV+hdJpW/rNW0gWlc4SXzF/kdrRaAVX9YCxKpziH7djeWT/HFAjLZcnY7NJD8xTeUEg== dependencies: - "@typescript-eslint/scope-manager" "4.4.0" - "@typescript-eslint/types" "4.4.0" - "@typescript-eslint/typescript-estree" "4.4.0" + "@typescript-eslint/scope-manager" "4.4.1" + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/typescript-estree" "4.4.1" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.4.0.tgz#2f3dd27692a12cc9a046a90ba6a9d8cb7731190a" - integrity sha512-r2FIeeU1lmW4K3CxgOAt8djI5c6Q/5ULAgdVo9AF3hPMpu0B14WznBAtxrmB/qFVbVIB6fSx2a+EVXuhSVMEyA== +"@typescript-eslint/scope-manager@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.4.1.tgz#d19447e60db2ce9c425898d62fa03b2cce8ea3f9" + integrity sha512-2oD/ZqD4Gj41UdFeWZxegH3cVEEH/Z6Bhr/XvwTtGv66737XkR4C9IqEkebCuqArqBJQSj4AgNHHiN1okzD/wQ== dependencies: - "@typescript-eslint/types" "4.4.0" - "@typescript-eslint/visitor-keys" "4.4.0" + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/visitor-keys" "4.4.1" -"@typescript-eslint/types@4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.4.0.tgz#63440ef87a54da7399a13bdd4b82060776e9e621" - integrity sha512-nU0VUpzanFw3jjX+50OTQy6MehVvf8pkqFcURPAE06xFNFenMj1GPEI6IESvp7UOHAnq+n/brMirZdR+7rCrlA== - -"@typescript-eslint/typescript-estree@2.19.0": - version "2.19.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.19.0.tgz#6bd7310b9827e04756fe712909f26956aac4b196" - integrity sha512-n6/Xa37k0jQdwpUszffi19AlNbVCR0sdvCs3DmSKMD7wBttKY31lhD2fug5kMD91B2qW4mQldaTEc1PEzvGu8w== - dependencies: - debug "^4.1.1" - eslint-visitor-keys "^1.1.0" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^6.3.0" - tsutils "^3.17.1" +"@typescript-eslint/types@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.4.1.tgz#c507b35cf523bc7ba00aae5f75ee9b810cdabbc1" + integrity sha512-KNDfH2bCyax5db+KKIZT4rfA8rEk5N0EJ8P0T5AJjo5xrV26UAzaiqoJCxeaibqc0c/IvZxp7v2g3difn2Pn3w== "@typescript-eslint/typescript-estree@2.34.0": version "2.34.0" @@ -3594,13 +3573,13 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.4.0.tgz#16a2df7c16710ddd5406b32b86b9c1124b1ca526" - integrity sha512-Fh85feshKXwki4nZ1uhCJHmqKJqCMba+8ZicQIhNi5d5jSQFteWiGeF96DTjO8br7fn+prTP+t3Cz/a/3yOKqw== +"@typescript-eslint/typescript-estree@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.4.1.tgz#598f6de488106c2587d47ca2462c60f6e2797cb8" + integrity sha512-wP/V7ScKzgSdtcY1a0pZYBoCxrCstLrgRQ2O9MmCUZDtmgxCO/TCqOTGRVwpP4/2hVfqMz/Vw1ZYrG8cVxvN3g== dependencies: - "@typescript-eslint/types" "4.4.0" - "@typescript-eslint/visitor-keys" "4.4.0" + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/visitor-keys" "4.4.1" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" @@ -3608,12 +3587,12 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.4.0.tgz#0a9118344082f14c0f051342a74b42dfdb012640" - integrity sha512-oBWeroUZCVsHLiWRdcTXJB7s1nB3taFY8WGvS23tiAlT6jXVvsdAV4rs581bgdEjOhn43q6ro7NkOiLKu6kFqA== +"@typescript-eslint/visitor-keys@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.4.1.tgz#1769dc7a9e2d7d2cfd3318b77ed8249187aed5c3" + integrity sha512-H2JMWhLaJNeaylSnMSQFEhT/S/FsJbebQALmoJxMPMxLtlVAMy2uJP/Z543n9IizhjRayLSqoInehCeNW9rWcw== dependencies: - "@typescript-eslint/types" "4.4.0" + "@typescript-eslint/types" "4.4.1" eslint-visitor-keys "^2.0.0" "@uppy/companion-client@^1.4.1", "@uppy/companion-client@^1.5.4": @@ -4292,10 +4271,10 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: - version "6.12.5" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" - integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -4646,11 +4625,6 @@ ast-types-flow@0.0.7, ast-types-flow@^0.0.7: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= -ast-types@0.11.3: - version "0.11.3" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8" - integrity sha512-XA5o5dsNw8MhyW0Q7MWXJWc4oOzZKbdsEJq45h7c8q/d9DwWZ5F2ugUc1PuMLPGsUnphCt/cNDHu8JeBbxf1qA== - ast-types@0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.3.tgz#50da3f28d17bdbc7969a3a2d83a0e4a72ae755a7" @@ -4663,6 +4637,13 @@ ast-types@^0.13.2: dependencies: tslib "^2.0.1" +ast-types@^0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd" + integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA== + dependencies: + tslib "^2.0.1" + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -4994,13 +4975,13 @@ babel-plugin-named-asset-import@^0.3.1, babel-plugin-named-asset-import@^0.3.6: integrity sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA== babel-plugin-react-docgen@^4.0.0, babel-plugin-react-docgen@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.2.0.tgz#4f425692f0ca06c73a1462274d370a3ac0637b46" - integrity sha512-B3tjZwKskcia9TsqkND+9OTjl/F5A5OBvRJ6Ktg34CONoxm+kB3CJ52wk5TjbszX9gqCPcAuc0GgkhT0CLuT/Q== + version "4.2.1" + resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.2.1.tgz#7cc8e2f94e8dc057a06e953162f0810e4e72257b" + integrity sha512-UQ0NmGHj/HAqi5Bew8WvNfCk8wSsmdgNd8ZdMjBCICtyCJCq9LiqgqvjCYe570/Wg7AQArSq1VQ60Dd/CHN7mQ== dependencies: + ast-types "^0.14.2" lodash "^4.17.15" react-docgen "^5.0.0" - recast "^0.14.7" "babel-plugin-styled-components@>= 1", babel-plugin-styled-components@^1.10.7: version "1.11.1" @@ -5685,9 +5666,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: - version "1.0.30001146" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz#c61fcb1474520c1462913689201fb292ba6f447c" - integrity sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug== + version "1.0.30001148" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001148.tgz#dc97c7ed918ab33bf8706ddd5e387287e015d637" + integrity sha512-E66qcd0KMKZHNJQt9hiLZGE3J4zuTqE1OnU53miEVtylFbwOEmeA5OsRu90noZful+XGSQOni1aT2tiqu/9yYw== capture-exit@^2.0.0: version "2.0.0" @@ -5829,9 +5810,9 @@ chokidar@^2.0.4, chokidar@^2.1.8: fsevents "^1.2.7" chokidar@^3.3.0, chokidar@^3.4.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" - integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + version "3.4.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" + integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -5839,7 +5820,7 @@ chokidar@^3.3.0, chokidar@^3.4.1: is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.4.0" + readdirp "~3.5.0" optionalDependencies: fsevents "~2.1.2" @@ -6077,21 +6058,21 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== +color-string@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" + integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" color@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" - integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== dependencies: color-convert "^1.9.1" - color-string "^1.5.2" + color-string "^1.5.4" colorette@^1.2.1: version "1.2.1" @@ -6602,9 +6583,9 @@ css-what@2.1: integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== css-what@^3.2.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e" - integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g== + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== css.escape@^1.5.1: version "1.5.1" @@ -7170,9 +7151,9 @@ doctypes@^1.1.0: integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= dom-accessibility-api@^0.5.1: - version "0.5.3" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.3.tgz#0ea493c924d4070dfbf531c4aaca3d7a2c601aab" - integrity sha512-yfqzAi1GFxK6EoJIZKgxqJyK6j/OjEFEUi2qkNThD/kUhoCFSG1izq31B5xuxzbJBGw9/67uPtkPMYAzWL7L7Q== + version "0.5.4" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166" + integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== dom-converter@^0.2: version "0.2.0" @@ -7349,9 +7330,9 @@ ejs@^3.1.5: jake "^10.6.1" electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.571: - version "1.3.578" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0" - integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q== + version "1.3.579" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.579.tgz#58bf17499de6edf697e1442017d8569bce0d301a" + integrity sha512-9HaGm4UDxCtcmIqWWdv79pGgpRZWTqr+zg6kxp0MelSHfe1PNjrI8HXy1HgTSy4p0iQETGt8/ElqKFLW008BSA== elegant-spinner@^1.0.1: version "1.0.1" @@ -7587,9 +7568,9 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3: ext "^1.1.2" escalade@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" - integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" @@ -7740,15 +7721,15 @@ eslint-plugin-react@7.19.0: xregexp "^4.3.0" eslint-plugin-react@^7.21.3: - version "7.21.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.3.tgz#71655d2af5155b19285ec929dd2cdc67a4470b52" - integrity sha512-OI4GwTCqyIb4ipaOEGLWdaOHCXZZydStAsBEPB2e1ZfNM37bojpgO1BoOQbFb0eLVz3QLDx7b+6kYcrxCuJfhw== + version "7.21.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.4.tgz#31060b2e5ff82b12e24a3cc33edb7d12f904775c" + integrity sha512-uHeQ8A0hg0ltNDXFu3qSfFqTNPXm1XithH6/SY318UX76CMj7Q599qWpgmMhVQyvhq36pm7qvoN3pb6/3jsTFg== dependencies: array-includes "^3.1.1" array.prototype.flatmap "^1.2.3" doctrine "^2.1.0" has "^1.0.3" - jsx-ast-utils "^2.4.1" + jsx-ast-utils "^2.4.1 || ^3.0.0" object.entries "^1.1.2" object.fromentries "^2.0.2" object.values "^1.1.1" @@ -11056,7 +11037,7 @@ jstransformer@1.0.0: is-promise "^2.0.0" promise "^7.0.1" -jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3, jsx-ast-utils@^2.4.1: +jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3: version "2.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e" integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w== @@ -11064,6 +11045,14 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3, jsx-ast-utils@^2.4.1: array-includes "^3.1.1" object.assign "^4.1.0" +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz#642f1d7b88aa6d7eb9d8f2210e166478444fa891" + integrity sha512-d4/UOjg+mxAWxCiF0c5UTSwyqbchkbqCvK87aBovhnh8GtysTjWmgC63tY0cJx/HzGgm9qnA147jVBdpOiQ2RA== + dependencies: + array-includes "^3.1.1" + object.assign "^4.1.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -11543,6 +11532,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + magic-string@^0.25.0, magic-string@^0.25.5, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -12195,9 +12189,9 @@ namespace-emitter@^2.0.1: integrity sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g== nan@^2.12.1, nan@^2.13.2: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== nanoid@^2.0.4: version "2.1.11" @@ -13977,11 +13971,11 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: react-is "^16.12.0" pretty-format@^26.4.2: - version "26.4.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" - integrity sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA== + version "26.5.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.5.2.tgz#5d896acfdaa09210683d34b6dc0e6e21423cd3e1" + integrity sha512-VizyV669eqESlkOikKJI8Ryxl/kPpbdLwNdPs2GrbQs18MpySB5S0Yo0N7zkg2xTRiFq4CFw8ct5Vg4a0xP0og== dependencies: - "@jest/types" "^26.3.0" + "@jest/types" "^26.5.2" ansi-regex "^5.0.0" ansi-styles "^4.0.0" react-is "^16.12.0" @@ -13992,9 +13986,9 @@ pretty-hrtime@^1.0.3: integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= prismjs@^1.21.0, prismjs@^1.8.4: - version "1.21.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3" - integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw== + version "1.22.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.22.0.tgz#73c3400afc58a823dd7eed023f8e1ce9fd8977fa" + integrity sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w== optionalDependencies: clipboard "^2.0.0" @@ -14005,7 +13999,7 @@ prismjs@~1.17.0: optionalDependencies: clipboard "^2.0.0" -private@^0.1.8, private@~0.1.5: +private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== @@ -14087,9 +14081,9 @@ prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, react-is "^16.8.1" property-information@^5.0.0, property-information@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.5.0.tgz#4dc075d493061a82e2b7d096f406e076ed859943" - integrity sha512-RgEbCx2HLa1chNgvChcx+rrCWD0ctBmGSE0M7lVm1yyv4UbvbrWoXp/BkVLZefzjrRBGW8/Js6uh/BnlHXFyjA== + version "5.6.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" + integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== dependencies: xtend "^4.0.0" @@ -14407,9 +14401,9 @@ react-app-polyfill@^1.0.6: whatwg-fetch "^3.0.0" react-base-table@^1.9.1: - version "1.11.3" - resolved "https://registry.yarnpkg.com/react-base-table/-/react-base-table-1.11.3.tgz#9b77b84347e905ec68bd0b5e9639ac4c41b53e5e" - integrity sha512-JBYtRQBVrNxdP2QpuRyi3+FXRViTmNuCHeV6LwTpWY10x/AwqtGmamNdtYI5hZ95f8lIvo2nTfZeRgOmdyveQw== + version "1.12.0" + resolved "https://registry.yarnpkg.com/react-base-table/-/react-base-table-1.12.0.tgz#3fc39f1cc7d1a4b349572e1d2b283626987d5dcc" + integrity sha512-CaS8iI8JyK5rGjggfupZWKBPypNxlXrjOi+ndUsLMCJaoKCgUXZSlYv2oihq44c10MgaH/eEgEvkMV6dIGa59w== dependencies: "@babel/runtime" "^7.0.0" classnames "^2.2.5" @@ -14959,9 +14953,9 @@ react-syntax-highlighter@^11.0.2: refractor "^2.4.1" react-table@^7.0.0: - version "7.5.2" - resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.5.2.tgz#d82ceee3d4d40b119159bce1708f084a95d3435c" - integrity sha512-qiceR/gQUOBsO/q1z1wZ3RbRvkRt39Gbzo631HiPuWmo+eTmTgaXDqLGzCmU+bOr81PB6kDxXhoWQR8hiWaUJA== + version "7.6.0" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.6.0.tgz#83d765b96505b5332d108a2e0c27ab653f5a78c3" + integrity sha512-16kRTypBWz9ZwLnPWA8hc3eIC64POzO9GaMBiKaCcVM+0QOQzt0G7ebzGUM8SW0CYUpVM+glv1kMXrWj9tr3Sw== react-tabs@^3.0.0: version "3.1.1" @@ -15159,10 +15153,10 @@ readdirp@~3.2.0: dependencies: picomatch "^2.0.4" -readdirp@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" - integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== dependencies: picomatch "^2.2.1" @@ -15183,16 +15177,6 @@ recast@0.19.1: private "^0.1.8" source-map "~0.6.1" -recast@^0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" - integrity sha512-/nwm9pkrcWagN40JeJhkPaRxiHXBRkXyRh/hgU088Z/v+qCy+zIHHY6bC6o7NaKAxPqtE6nD8zBH1LfU0/Wx6A== - dependencies: - ast-types "0.11.3" - esprima "~4.0.0" - private "~0.1.5" - source-map "~0.6.1" - recast@^0.18.1: version "0.18.10" resolved "https://registry.yarnpkg.com/recast/-/recast-0.18.10.tgz#605ebbe621511eb89b6356a7e224bff66ed91478" @@ -15927,6 +15911,15 @@ schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6 ajv "^6.12.4" ajv-keywords "^3.5.2" +schema-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" + integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + dependencies: + "@types/json-schema" "^7.0.6" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + scriptjs@^2.5.8: version "2.5.9" resolved "https://registry.yarnpkg.com/scriptjs/-/scriptjs-2.5.9.tgz#343915cd2ec2ed9bfdde2b9875cd28f59394b35f" @@ -17305,14 +17298,14 @@ ts-pnp@^1.1.2, ts-pnp@^1.1.6: integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: - version "1.14.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6" - integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.0.0, tslib@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.2.tgz#462295631185db44b21b1ea3615b63cd1c038242" - integrity sha512-wAH28hcEKwna96/UacuWaVspVLkg4x1aDM9JlzqaQTOFczCktkVAb5fmXChgandR1EraDPs2w8P+ozM+oafwxg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== tslib@~1.13.0: version "1.13.0" @@ -17837,9 +17830,9 @@ void-elements@^3.1.0: integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= vue-docgen-api@^4.1.0: - version "4.32.4" - resolved "https://registry.yarnpkg.com/vue-docgen-api/-/vue-docgen-api-4.32.4.tgz#a74f4dfce99a078a4a87dcb779c6cdd480eeab01" - integrity sha512-dtPOg9uCnclBOiWASMDMBjYhrXbRDlADZNTMkc5NtF1eSXzltN7GvB7P3YO92M5IvUeHwq84A9BqGBBbye5oWg== + version "4.33.0" + resolved "https://registry.yarnpkg.com/vue-docgen-api/-/vue-docgen-api-4.33.0.tgz#59630636373ee3f06b0a36820d6235fdb9adfbae" + integrity sha512-kNIAE8kKsZtOqROqDyMGa30AVGcwo2D2g+Z2thuVbEcOAqRc4eM72PoFwqX9ZRqQSdj4TnqIGI3EfF4KHTZ/EQ== dependencies: "@babel/parser" "^7.6.0" "@babel/types" "^7.6.0" @@ -17851,7 +17844,7 @@ vue-docgen-api@^4.1.0: pug "^3.0.0" recast "0.19.1" ts-map "^1.0.3" - vue-inbrowser-compiler-utils "^4.32.1" + vue-inbrowser-compiler-utils "^4.33.0" vue-docgen-loader@^1.3.0-beta.0: version "1.5.0" @@ -17863,10 +17856,10 @@ vue-docgen-loader@^1.3.0-beta.0: loader-utils "^1.2.3" querystring "^0.2.0" -vue-inbrowser-compiler-utils@^4.32.1: - version "4.32.1" - resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.32.1.tgz#d8774a4b7e91677d4d17d441485f5eafc77bc65d" - integrity sha512-IL8rBV3lCyHErqD8sBdQhWz3zJ/wLzG6JfoSzZ3K6HShS5QqIQfJN0GESvzIos6EGvmtByEf4TTJnjm12b51VQ== +vue-inbrowser-compiler-utils@^4.33.0: + version "4.33.0" + resolved "https://registry.yarnpkg.com/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.33.0.tgz#3bb510a7931c62d076a035f71dc31a5329e4e015" + integrity sha512-9VxLYc+J8o3Z1H6GpnPYD9J2lMcP4ahi1BoYTcRM6Yw0F/17Gbypog3yVCyh1VO4uDoIldi4b9ZF33zvR5KEvg== dependencies: camelcase "^5.3.1" @@ -18522,6 +18515,14 @@ worker-loader@^2.0.0: loader-utils "^1.0.0" schema-utils "^0.4.0" +worker-loader@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.4.tgz#bbc0af0af7e2d972c1105001ab215497c00b98ca" + integrity sha512-puFIebctLf/xB5Vex9QTX4Zr+wR6hQZqgVEg7QeUTA0I8XzTmGr620ryvY8E448C/hJ+eE+NKiIX9xyFcQNFbQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + worker-rpc@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5" @@ -18529,13 +18530,6 @@ worker-rpc@^0.1.0: dependencies: microevent.ts "~0.1.1" -workerize-loader@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/workerize-loader/-/workerize-loader-1.3.0.tgz#4995cf2ff2b45dd6dc60e4411e63f5ae2c704d36" - integrity sha512-utWDc8K6embcICmRBUUkzanPgKBb8yM1OHfh6siZfiMsswE8wLCa9CWS+L7AARz0+Th4KH4ZySrqer/OJ9WuWw== - dependencies: - loader-utils "^2.0.0" - wrap-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/pluginExceptions/StaleConnectionException.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/pluginExceptions/StaleConnectionException.java index 2c17c9acac5..26a46913bf6 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/pluginExceptions/StaleConnectionException.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/pluginExceptions/StaleConnectionException.java @@ -1,4 +1,14 @@ package com.appsmith.external.pluginExceptions; public class StaleConnectionException extends RuntimeException { + public StaleConnectionException() { + } + + public StaleConnectionException(String message) { + super(message); + } + + public StaleConnectionException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/app/server/appsmith-plugins/dynamoPlugin/plugin.properties b/app/server/appsmith-plugins/dynamoPlugin/plugin.properties new file mode 100644 index 00000000000..9422bbfc28d --- /dev/null +++ b/app/server/appsmith-plugins/dynamoPlugin/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=dynamo-plugin +plugin.class=com.external.plugins.DynamoPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= diff --git a/app/server/appsmith-plugins/dynamoPlugin/pom.xml b/app/server/appsmith-plugins/dynamoPlugin/pom.xml new file mode 100644 index 00000000000..75bfe75eb3b --- /dev/null +++ b/app/server/appsmith-plugins/dynamoPlugin/pom.xml @@ -0,0 +1,141 @@ + + + + 4.0.0 + + com.external.plugins + dynamoPlugin + 1.0-SNAPSHOT + + dynamoPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + dynamo-plugin + com.external.plugins.DynamoPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + + org.pf4j + pf4j-spring + 0.6.0 + provided + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + provided + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + software.amazon.awssdk + dynamodb + 2.15.3 + compile + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + junit + junit + 4.13.1 + test + + + + org.testcontainers + testcontainers + 1.15.0-rc2 + test + + + + io.projectreactor + reactor-test + 3.2.11.RELEASE + test + + + org.mockito + mockito-core + 3.1.0 + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + + + + + + + package + + shade + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib + + + + + + + + diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java new file mode 100644 index 00000000000..54fc8357cd6 --- /dev/null +++ b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java @@ -0,0 +1,364 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.BooleanUtils; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkField; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder; +import software.amazon.awssdk.services.dynamodb.model.DynamoDbResponse; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +public class DynamoPlugin extends BasePlugin { + + public DynamoPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + /** + * Dynamo plugin receives the query as json of the following format: + * { + * "action": "GetItem", + * "parameters": {...} // Depends on the action above. + * } + * + * DynamoDB actions and parameters reference: + * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Operations_Amazon_DynamoDB.html + */ + + @Slf4j + @Extension + public static class DynamoPluginExecutor implements PluginExecutor { + + @Override + public Mono execute(DynamoDbClient ddb, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + + ActionExecutionResult result = new ActionExecutionResult(); + + final String action = actionConfiguration.getPath(); + if (StringUtils.isEmpty(action)) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Missing action name (like `ListTables`, `GetItem` etc.)." + )); + } + + final String body = actionConfiguration.getBody(); + Map parameters = null; + try { + if (!StringUtils.isEmpty(body)) { + parameters = objectMapper.readValue(body, HashMap.class); + } + } catch (IOException e) { + final String message = "Error parsing the JSON body: " + e.getMessage(); + log.warn(message, e); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, message)); + } + + final Class requestClass; + try { + requestClass = Class.forName("software.amazon.awssdk.services.dynamodb.model." + action + "Request"); + } catch (ClassNotFoundException e) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Unknown action: `" + action + "`. Note that action names are case-sensitive." + )); + } + + try { + final Method actionExecuteMethod = DynamoDbClient.class.getMethod( + // Convert `ListTables` to `listTables`, which is the name of the method to execute this action. + toLowerCamelCase(action), + requestClass + ); + final DynamoDbResponse response = (DynamoDbResponse) actionExecuteMethod.invoke(ddb, plainToSdk(parameters, requestClass)); + result.setBody(sdkToPlain(response)); + } catch (AppsmithPluginException | InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) { + final String message = "Error executing the DynamoDB Action: " + (e.getCause() == null ? e : e.getCause()).getMessage(); + log.warn(message, e); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, message)); + } + + result.setIsExecutionSuccess(true); + return Mono.just(result); + } + + @Override + public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + final DynamoDbClientBuilder builder = DynamoDbClient.builder(); + + if (!CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + final Endpoint endpoint = datasourceConfiguration.getEndpoints().get(0); + builder.endpointOverride(URI.create("http://" + endpoint.getHost() + ":" + endpoint.getPort())); + } + + final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + if (authentication == null || StringUtils.isEmpty(authentication.getDatabaseName())) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Missing region in datasource." + )); + } + + builder.region(Region.of(authentication.getDatabaseName())); + + builder.credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(authentication.getUsername(), authentication.getPassword()) + )); + + return Mono.justOrEmpty(builder.build()); + } + + @Override + public void datasourceDestroy(DynamoDbClient client) { + if (client != null) { + client.close(); + } + } + + @Override + public Set validateDatasource(@NonNull DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + + final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + if (authentication == null) { + invalids.add("Missing AWS Access Key ID and Secret Access Key."); + } else { + if (StringUtils.isEmpty(authentication.getUsername())) { + invalids.add("Missing AWS Access Key ID."); + } + + if (StringUtils.isEmpty(authentication.getPassword())) { + invalids.add("Missing AWS Secret Access Key."); + } + + if (StringUtils.isEmpty(authentication.getDatabaseName())) { + invalids.add("Missing region configuration."); + } + } + + return invalids; + } + + @Override + public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { + return datasourceCreate(datasourceConfiguration) + .map(client -> { + client.close(); + return true; + }) + .defaultIfEmpty(false) + .map(isValid -> BooleanUtils.isTrue(isValid) + ? new DatasourceTestResult() + : new DatasourceTestResult("Unable to create DynamoDB Client.") + ); + } + + } + + private static String toLowerCamelCase(String action) { + return action.substring(0, 1).toLowerCase() + action.substring(1); + } + + /** + * Given a map that conforms to what a valid DynamoDB request should look like, this function will convert into + * a DynamoDBRequest object from AWS SDK. This is done using Java's reflection API. + * @param mapping Mapping object representing the request details. + * @param type Request type that should be created. Eg., ListTablesRequest.class, PutItemRequest.class etc. + * @param Type param of the request class. + * @return An object of the request class, containing details of the request from the mapping. + * @throws IllegalAccessException Thrown if any of the SDK methods' contracts change. + * @throws InvocationTargetException Thrown if any of the SDK methods' contracts change. + * @throws NoSuchMethodException Thrown if any of the SDK methods' contracts change. + * @throws ClassNotFoundException Thrown if any of the builder class could not be found corresponding to the action class. + */ + public static T plainToSdk(Map mapping, Class type) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, + AppsmithPluginException, ClassNotFoundException { + + final Class builderType = Class.forName(type.getName() + "$Builder"); + + final Object builder = type.getMethod("builder").invoke(null); + + if (mapping != null) { + for (final Map.Entry entry : mapping.entrySet()) { + final String setterName = getSetterMethodName(entry.getKey()); + Object value = entry.getValue(); + + if (value instanceof String) { + // AWS SDK has two data types that are represented as Strings in JSON, namely strings and binary. + // We look at the parameter types for the setter method to decide which it should be, and then set + // convert the value if needed before calling the setter. + final Method setterMethod = findMethod(builderType, method -> { + final Class[] parameterTypes = method.getParameterTypes(); + return method.getName().equals(setterName) + && (SdkBytes.class.isAssignableFrom(parameterTypes[0]) || String.class.isAssignableFrom(parameterTypes[0])); + }); + if (setterMethod == null) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Invalid attribute/value by name " + entry.getKey() + ); + } + if (SdkBytes.class.isAssignableFrom(setterMethod.getParameterTypes()[0])) { + value = SdkBytes.fromUtf8String((String) value); + } + setterMethod.invoke(builder, value); + + } else if (value instanceof Boolean + || value instanceof Integer + || value instanceof Float + || value instanceof Double) { + // These data types have a setter method that takes a the value as is. Nothing fancy here. + builderType.getMethod(setterName, value.getClass()).invoke(builder, value); + + } else if (value instanceof Map) { + // For maps, we go recursive, applying this transformation to each value, and replacing with the + // result in the map. Generic types in the setter method's signature are used to convert the values. + final Method setterMethod = findMethod(builderType, m -> m.getName().equals(setterName)); + final ParameterizedType valueType = (ParameterizedType) setterMethod.getGenericParameterTypes()[0]; + final Map transformedMap = new HashMap<>(); + for (final Map.Entry innerEntry : ((Map) value).entrySet()) { + Object innerValue = innerEntry.getValue(); + if (innerValue instanceof Map) { + innerValue = plainToSdk((Map) innerValue, (Class) valueType.getActualTypeArguments()[1]); + } + transformedMap.put(innerEntry.getKey(), innerValue); + } + value = transformedMap; + if (!Map.class.isAssignableFrom((Class) valueType.getRawType())) { + // Some setters don't take a plain map. For example, some require an `AttributeValue` instance + // for objects that are just maps in JSON. So, we make that conversion here. + value = plainToSdk((Map) value, (Class) valueType.getRawType()); + } + setterMethod.invoke(builder, value); + + } else if (value instanceof Collection) { + // For linear collections, the process is similar to that of maps. + final Collection valueAsCollection = (Collection) value; + // Find method by name and exclude the varargs version of the method. + final Method setterMethod = findMethod(builderType, m -> m.getName().equals(setterName) && !m.getParameterTypes()[0].getName().startsWith("[L")); + final ParameterizedType valueType = (ParameterizedType) setterMethod.getGenericParameterTypes()[0]; + final Collection reTypedList = new ArrayList<>(); + for (final Object innerValue : valueAsCollection) { + if (innerValue instanceof Map) { + reTypedList.add(plainToSdk((Map) innerValue, (Class) valueType.getActualTypeArguments()[0])); + } else if (innerValue instanceof String && SdkBytes.class.isAssignableFrom((Class) valueType.getActualTypeArguments()[0])) { + reTypedList.add(SdkBytes.fromUtf8String((String) innerValue)); + } else { + reTypedList.add(innerValue); + } + } + setterMethod.invoke(builder, reTypedList); + + } else { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Unknown value type while deserializing:" + value.getClass().getName() + ); + + } + } + } + + return (T) builderType.getMethod("build").invoke(builder); + } + + private static Method findMethod(Class builderType, Predicate predicate) { + return Arrays.stream(builderType.getMethods()) + .filter(predicate) + .findFirst() + .orElse(null); + } + + /** + * Computes the name of the setter method in AWS SDK that will set the value of the field given by the argument. + * @param key Name of the field for which to compute the setter method's name. + * @return Name of the setter method that will set the value of the given `key` field. + */ + private static String getSetterMethodName(final String key) { + if ("NULL".equals(key)) { + // Since `null` is a reserved word in Java, AWS SDK uses `nul` for this field. + return "nul"; + } else if (isUpperCase(key)) { + return key.toLowerCase(); + } else { + return toLowerCamelCase(key); + } + } + + private static Map sdkToPlain(SdkPojo response) { + final Map plain = new HashMap<>(); + + for (final SdkField field : response.sdkFields()) { + Object value = field.getValueOrDefault(response); + + if (value instanceof SdkPojo) { + value = sdkToPlain((SdkPojo) value); + + } else if (value instanceof Map) { + final Map valueAsMap = (Map) value; + final Map plainMap = new HashMap<>(); + for (final Map.Entry entry : valueAsMap.entrySet()) { + final var key = entry.getKey(); + Object innerValue = entry.getValue(); + if (innerValue instanceof SdkPojo) { + innerValue = sdkToPlain((SdkPojo) innerValue); + } + plainMap.put(key, innerValue); + } + value = plainMap; + + } + + plain.put(field.memberName(), value); + } + + return plain; + } + + private static boolean isUpperCase(String s) { + for (char c : s.toCharArray()) { + if (!Character.isUpperCase(c)) { + return false; + } + } + return true; + } + +} diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/dynamoPlugin/src/main/resources/editor.json new file mode 100644 index 00000000000..b7237754a4f --- /dev/null +++ b/app/server/appsmith-plugins/dynamoPlugin/src/main/resources/editor.json @@ -0,0 +1,188 @@ +{ + "editor": [ + { + "sectionName": "", + "id": 1, + "children": [ + { + "label": "Action", + "configProperty": "actionConfiguration.path", + "controlType": "DROP_DOWN", + "isRequired": true, + "initialValue": "GetItem", + "options": [ + { + "label": "BatchGetItem", + "value":"BatchGetItem" + }, + { + "label": "BatchWriteItem", + "value": "BatchWriteItem" + }, + { + "label": "CreateBackup", + "value": "CreateBackup" + }, + { + "label": "CreateGlobalTable", + "value": "CreateGlobalTable" + }, + { + "label": "CreateTable", + "value": "CreateTable" + }, + { + "label": "DeleteBackup", + "value": "DeleteBackup" + }, + { + "label": "DeleteItem", + "value": "DeleteItem" + }, + { + "label": "DeleteTable", + "value": "DeleteTable" + }, + { + "label": "DescribeBackup", + "value": "DescribeBackup" + }, + { + "label": "DescribeContinuousBackups", + "value": "DescribeContinuousBackups" + }, + { + "label": "DescribeContributorInsights", + "value": "DescribeContributorInsights" + }, + { + "label": "DescribeEndpoints", + "value": "DescribeEndpoints" + }, + { + "label": "DescribeGlobalTable", + "value": "DescribeGlobalTable" + }, + { + "label": "DescribeGlobalTableSettings", + "value": "DescribeGlobalTableSettings" + }, + { + "label": "DescribeLimits", + "value": "DescribeLimits" + }, + { + "label": "DescribeTable", + "value": "DescribeTable" + }, + { + "label": "DescribeTableReplicaAutoScaling", + "value": "DescribeTableReplicaAutoScaling" + }, + { + "label": "DescribeTimeToLive", + "value": "DescribeTimeToLive" + }, + { + "label": "GetItem", + "value": "GetItem" + }, + { + "label": "ListBackups", + "value": "ListBackups" + }, + { + "label": "ListContributorInsights", + "value": "ListContributorInsights" + }, + { + "label": "ListGlobalTables", + "value": "ListGlobalTables" + }, + { + "label": "ListTables", + "value": "ListTables" + }, + { + "label": "ListTagsOfResource", + "value": "ListTagsOfResource" + }, + { + "label": "PutItem", + "value": "PutItem" + }, + { + "label": "Query", + "value": "Query" + }, + { + "label": "RestoreTableFromBackup", + "value": "RestoreTableFromBackup" + }, + { + "label": "RestoreTableToPointInTime", + "value": "RestoreTableToPointInTime" + }, + { + "label": "Scan", + "value": "Scan" + }, + { + "label": "TagResource", + "value": "TagResource" + }, + { + "label": "TransactGetItems", + "value": "TransactGetItems" + }, + { + "label": "TransactWriteItems", + "value": "TransactWriteItems" + }, + { + "label": "UntagResource", + "value": "UntagResource" + }, + { + "label": "UpdateContinuousBackups", + "value": "UpdateContinuousBackups" + }, + { + "label": "UpdateContributorInsights", + "value": "UpdateContributorInsights" + }, + { + "label": "UpdateGlobalTable", + "value": "UpdateGlobalTable" + }, + { + "label": "UpdateGlobalTableSettings", + "value": "UpdateGlobalTableSettings" + }, + { + "label": "UpdateItem", + "value": "UpdateItem" + }, + { + "label": "UpdateTable", + "value": "UpdateTable" + }, + { + "label": "UpdateTableReplicaAutoScaling", + "value": "UpdateTableReplicaAutoScaling" + }, + { + "label": "UpdateTimeToLive", + "value": "UpdateTimeToLive" + } + ] + }, + { + "label": "", + "configProperty": "actionConfiguration.body", + "controlType": "QUERY_DYNAMIC_TEXT" + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/dynamoPlugin/src/main/resources/form.json new file mode 100644 index 00000000000..0f9c46951ed --- /dev/null +++ b/app/server/appsmith-plugins/dynamoPlugin/src/main/resources/form.json @@ -0,0 +1,177 @@ +{ + "form": [ + { + "sectionName": "Details", + "id": 1, + "children": [ + { + "label": "Region", + "configProperty": "datasourceConfiguration.authentication.databaseName", + "controlType": "DROP_DOWN", + "isRequired": true, + "initialValue": "AP_SOUTH_1", + "options": [ + { + "label": "ap-south-1", + "value": "ap-south-1" + }, + { + "label": "eu-south-1", + "value": "eu-south-1" + }, + { + "label": "us-gov-east-1", + "value": "us-gov-east-1" + }, + { + "label": "ca-central-1", + "value": "ca-central-1" + }, + { + "label": "eu-central-1", + "value": "eu-central-1" + }, + { + "label": "us-west-1", + "value": "us-west-1" + }, + { + "label": "us-west-2", + "value": "us-west-2" + }, + { + "label": "af-south-1", + "value": "af-south-1" + }, + { + "label": "eu-north-1", + "value": "eu-north-1" + }, + { + "label": "eu-west-3", + "value": "eu-west-3" + }, + { + "label": "eu-west-2", + "value": "eu-west-2" + }, + { + "label": "eu-west-1", + "value": "eu-west-1" + }, + { + "label": "ap-northeast-2", + "value": "ap-northeast-2" + }, + { + "label": "ap-northeast-1", + "value": "ap-northeast-1" + }, + { + "label": "me-south-1", + "value": "me-south-1" + }, + { + "label": "sa-east-1", + "value": "sa-east-1" + }, + { + "label": "ap-east-1", + "value": "ap-east-1" + }, + { + "label": "cn-north-1", + "value": "cn-north-1" + }, + { + "label": "us-gov-west-1", + "value": "us-gov-west-1" + }, + { + "label": "ap-southeast-1", + "value": "ap-southeast-1" + }, + { + "label": "ap-southeast-2", + "value": "ap-southeast-2" + }, + { + "label": "us-iso-east-1", + "value": "us-iso-east-1" + }, + { + "label": "us-east-1", + "value": "us-east-1" + }, + { + "label": "us-east-2", + "value": "us-east-2" + }, + { + "label": "cn-northwest-1", + "value": "cn-northwest-1" + }, + { + "label": "us-isob-east-1", + "value": "us-isob-east-1" + }, + { + "label": "aws-global", + "value": "aws-global" + }, + { + "label": "aws-cn-global", + "value": "aws-cn-global" + }, + { + "label": "aws-us-gov-global", + "value": "aws-us-gov-global" + }, + { + "label": "aws-iso-global", + "value": "aws-iso-global" + }, + { + "label": "aws-iso-b-global", + "value": "aws-iso-b-global" + } + ] + }, + { + "sectionName": "Endpoint Override (Overrides above region)", + "children": [ + { + "label": "Host Address (for overriding endpoint only)", + "configProperty": "datasourceConfiguration.endpoints[*].host", + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" + }, + { + "label": "Port", + "configProperty": "datasourceConfiguration.endpoints[*].port", + "dataType": "NUMBER", + "controlType": "KEYVALUE_ARRAY" + } + ] + }, + { + "label": "AWS Access Key ID", + "configProperty": "datasourceConfiguration.authentication.username", + "controlType": "INPUT_TEXT", + "isRequired": true, + "placeholderText": "", + "initialValue": "" + }, + { + "label": "AWS Secret Access Key", + "configProperty": "datasourceConfiguration.authentication.password", + "controlType": "INPUT_TEXT", + "isRequired": true, + "placeholderText": "", + "initialValue": "" + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java new file mode 100644 index 00000000000..cd55139d3d3 --- /dev/null +++ b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java @@ -0,0 +1,206 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.Endpoint; +import lombok.extern.log4j.Log4j; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Log4j +public class DynamoPluginTest { + + private final static DynamoPlugin.DynamoPluginExecutor pluginExecutor = new DynamoPlugin.DynamoPluginExecutor(); + + @SuppressWarnings("rawtypes") + @ClassRule + public static GenericContainer container = new GenericContainer(CompletableFuture.completedFuture("amazon/dynamodb-local")) + .withExposedPorts(8000); + + private final static DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + + @BeforeClass + public static void setUp() { + final String host = "localhost"; + final Integer port = container.getMappedPort(8000); + + final StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("dummy", "dummy") + ); + + DynamoDbClient ddb = DynamoDbClient.builder() + .region(Region.AP_SOUTH_1) + .endpointOverride(URI.create("http://" + host + ":" + port)) + .credentialsProvider(credentialsProvider) + .build(); + + ddb.createTable(CreateTableRequest.builder() + .tableName("cities") + .attributeDefinitions( + AttributeDefinition.builder().attributeName("Id").attributeType(ScalarAttributeType.S).build() + ) + .keySchema( + KeySchemaElement.builder().attributeName("Id").keyType(KeyType.HASH).build() + ) + .provisionedThroughput( + ProvisionedThroughput.builder().readCapacityUnits(5L).writeCapacityUnits(5L).build() + ) + .build()); + + ddb.putItem(PutItemRequest.builder() + .tableName("cities") + .item(Map.of( + "Id", AttributeValue.builder().s("1").build(), + "City", AttributeValue.builder().s("New Delhi").build() + )) + .build()); + + ddb.putItem(PutItemRequest.builder() + .tableName("cities") + .item(Map.of( + "Id", AttributeValue.builder().s("2").build(), + "City", AttributeValue.builder().s("Bangalore").build() + )) + .build()); + + System.out.println(ddb.listTables()); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost(host); + endpoint.setPort(port.longValue()); + dsConfig.setAuthentication(new AuthenticationDTO()); + dsConfig.getAuthentication().setUsername("dummy"); + dsConfig.getAuthentication().setPassword("dummy"); + dsConfig.getAuthentication().setDatabaseName(Region.AP_SOUTH_1.toString()); + dsConfig.setEndpoints(List.of(endpoint)); + } + + private Mono execute(String action, String jsonActionConfiguration) { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setPath(action); + actionConfiguration.setBody(jsonActionConfiguration); + + return pluginExecutor + .datasourceCreate(dsConfig) + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + } + + @Test + public void testListTables() { + StepVerifier.create(execute("ListTables", null)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + assertArrayEquals( + new String[]{"cities"}, + ((Map>) result.getBody()).get("TableNames").toArray() + ); + }) + .verifyComplete(); + } + + @Test + public void testGetItem() { + final String body = "{\n" + + " \"TableName\": \"cities\",\n" + + " \"Key\": {\n" + + " \"Id\": {\n" + + " \"S\": \"1\"\n" + + " }\n" + + " }\n" + + "}\n"; + + StepVerifier.create(execute("GetItem", body)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final Map> item = ((Map>>) result.getBody()).get("Item"); + assertEquals("New Delhi", item.get("City").get("S")); + }) + .verifyComplete(); + } + + @Test + public void testPutItem() { + final String body = "{\n" + + " \"TableName\": \"cities\",\n" + + " \"Item\": {\n" + + " \"Id\": {\n" + + " \"S\": \"9\"\n" + + " },\n" + + " \"City\": {\n" + + " \"S\": \"Mumbai\"\n" + + " }\n" + + " }\n" + + "}\n"; + + StepVerifier.create(execute("PutItem", body)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + assertNotNull(((Map>) result.getBody()).get("Attributes")); + }) + .verifyComplete(); + } + + @Test + public void testUpdateItem() { + final String body = "{\n" + + " \"TableName\": \"cities\",\n" + + " \"Key\": {\n" + + " \"Id\": {\n" + + " \"S\": \"2\"\n" + + " }\n" + + " },\n" + + " \"UpdateExpression\": \"set City = :new_city\",\n" + + " \"ExpressionAttributeValues\": {\n" + + " \":new_city\": {\n" + + " \"S\": \"Bengaluru\"\n" + + " }\n" + + " },\n" + + " \"ReturnValues\": \"ALL_NEW\"\n" + + "}\n"; + + StepVerifier.create(execute("UpdateItem", body)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final Map> attributes = ((Map>>) result.getBody()).get("Attributes"); + assertEquals("Bengaluru", attributes.get("City").get("S")); + }) + .verifyComplete(); + } + +} diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/PlainToSdkTests.java b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/PlainToSdkTests.java new file mode 100644 index 00000000000..6bf54ddd7f8 --- /dev/null +++ b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/PlainToSdkTests.java @@ -0,0 +1,129 @@ +package com.external.plugins; + +import org.junit.Test; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ListTablesRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +import java.util.List; +import java.util.Map; + +import static com.external.plugins.DynamoPlugin.plainToSdk; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class PlainToSdkTests { + + @Test + public void testListTablesNull() throws Exception { + final ListTablesRequest request = plainToSdk( + null, + ListTablesRequest.class + ); + + assertNotNull(request); + assertNull(request.exclusiveStartTableName()); + assertNull(request.limit()); + } + + @Test + public void testListTables() throws Exception { + final ListTablesRequest request = plainToSdk( + Map.of( + "ExclusiveStartTableName", "table_name", + "Limit", 1 + ), + ListTablesRequest.class + ); + + assertNotNull(request); + assertEquals("table_name", request.exclusiveStartTableName()); + assertEquals(1, request.limit().intValue()); + } + + @Test + public void testPutItem() throws Exception { + final PutItemRequest request = plainToSdk( + Map.of( + "ConditionalOperator", "conditional operator value", + "ConditionExpression", "conditional expression value", + "ExpressionAttributeNames", Map.of( + "#P", "Percentile" + ), + "ExpressionAttributeValues", Map.of( + ":token1", Map.of("S", "value1"), + ":token2", Map.of("N", "42") + ), + "Item", Map.of( + "one", Map.of("B", "binary blob as a string"), + "two", Map.of("BOOL", true), + "three", Map.of("BS", List.of("binary blob 1", "binary blob 2")), + "four", Map.of("L", List.of( + Map.of("S", "string in list 1"), + Map.of("S", "string in list 2"), + Map.of("S", "string in list 3") + )), + "five", Map.of("M", Map.of( + "key1", "val1", + "key2", "val2" + )), + "six", Map.of("N", "1234"), + "seven", Map.of("NS", List.of("12", "34", "56")), + "eight", Map.of("NULL", true), + "nine", Map.of("S", "string value") + ), + "TableName", "table_name" + ), + PutItemRequest.class + ); + + assertNotNull(request); + assertEquals("conditional operator value", request.conditionalOperatorAsString()); + assertEquals("conditional expression value", request.conditionExpression()); + assertEquals(9, request.item().size()); + assertEquals("table_name", request.tableName()); + } + + @Test + public void testGetItem() throws Exception { + final GetItemRequest request = plainToSdk( + Map.of( + "AttributesToGet", List.of("one", "two", "three"), + "ConsistentRead", true, + "ExpressionAttributeNames", Map.of( + "#P", "Percentile" + ), + "Key", Map.of( + "one", Map.of("B", "binary blob as a string"), + "two", Map.of("BOOL", true), + "three", Map.of("BS", List.of("binary blob 1", "binary blob 2")), + "four", Map.of("L", List.of( + Map.of("S", "string in list 1"), + Map.of("S", "string in list 2"), + Map.of("S", "string in list 3") + )), + "five", Map.of("M", Map.of( + "key1", "val1", + "key2", "val2" + )), + "six", Map.of("N", "1234"), + "seven", Map.of("NS", List.of("12", "34", "56")), + "eight", Map.of("NULL", true), + "nine", Map.of("S", "string value") + ), + "TableName", "table_name" + ), + GetItemRequest.class + ); + + assertNotNull(request); + assertArrayEquals(new String[]{"one", "two", "three"}, request.attributesToGet().toArray()); + assertTrue(request.consistentRead()); + assertEquals(9, request.key().size()); + assertEquals("table_name", request.tableName()); + } + +} diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/plugin.properties b/app/server/appsmith-plugins/elasticSearchPlugin/plugin.properties new file mode 100644 index 00000000000..2d0f796852a --- /dev/null +++ b/app/server/appsmith-plugins/elasticSearchPlugin/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=elasticsearch-plugin +plugin.class=com.external.plugins.ElasticSearchPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/pom.xml b/app/server/appsmith-plugins/elasticSearchPlugin/pom.xml new file mode 100644 index 00000000000..6edd487022c --- /dev/null +++ b/app/server/appsmith-plugins/elasticSearchPlugin/pom.xml @@ -0,0 +1,132 @@ + + + + 4.0.0 + + com.external.plugins + elasticSearchPlugin + 1.0-SNAPSHOT + + elasticSearchPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + elasticsearch-plugin + com.external.plugins.ElasticSearchPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + + org.pf4j + pf4j-spring + 0.6.0 + provided + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + provided + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + org.elasticsearch.client + elasticsearch-rest-client + 7.9.2 + + + + + junit + junit + 4.13.1 + test + + + + io.projectreactor + reactor-test + 3.3.5.RELEASE + test + + + + org.testcontainers + testcontainers + 1.15.0-rc2 + test + + + + org.testcontainers + elasticsearch + 1.15.0-rc2 + test + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + + + + + + + package + + shade + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib + + + + + + + + diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java new file mode 100644 index 00000000000..82ef9d9a760 --- /dev/null +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java @@ -0,0 +1,202 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.Header; +import org.apache.http.HttpHost; +import org.apache.http.StatusLine; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.message.BasicHeader; +import org.apache.http.nio.entity.NStringEntity; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ElasticSearchPlugin extends BasePlugin { + + public ElasticSearchPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Slf4j + @Extension + public static class ElasticSearchPluginExecutor implements PluginExecutor { + + @Override + public Mono execute(RestClient client, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + final ActionExecutionResult result = new ActionExecutionResult(); + + String body = actionConfiguration.getBody(); + + final String path = actionConfiguration.getPath(); + final Request request = new Request(actionConfiguration.getHttpMethod().toString(), path); + ContentType contentType = ContentType.APPLICATION_JSON; + + if (isBulkQuery(path)) { + contentType = ContentType.create("application/x-ndjson"); + + // If body is a JSON Array, convert it to an ND-JSON string. + if (body != null && body.trim().startsWith("[")) { + final StringBuilder ndJsonBuilder = new StringBuilder(); + try { + List commands = objectMapper.readValue(body, ArrayList.class); + for (Object object : commands) { + ndJsonBuilder.append(objectMapper.writeValueAsString(object)).append("\n"); + } + } catch (IOException e) { + final String message = "Error converting array to ND-JSON: " + e.getMessage(); + log.warn(message, e); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, message)); + } + body = ndJsonBuilder.toString(); + } + } + + if (body != null) { + request.setEntity(new NStringEntity(body, contentType)); + } + + try { + final String responseBody = new String( + client.performRequest(request).getEntity().getContent().readAllBytes()); + result.setBody(objectMapper.readValue(responseBody, HashMap.class)); + } catch (IOException e) { + final String message = "Error performing request: " + e.getMessage(); + log.warn(message, e); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, message)); + } + + result.setIsExecutionSuccess(true); + return Mono.just(result); + } + + private static boolean isBulkQuery(String path) { + return path.split("\\?", 1)[0].matches(".*\\b_bulk$"); + } + + @Override + public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + final List hosts = new ArrayList<>(); + + for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + hosts.add(new HttpHost(endpoint.getHost(), endpoint.getPort().intValue(), "http")); + } + + final RestClientBuilder clientBuilder = RestClient.builder(hosts.toArray(new HttpHost[]{})); + + final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + if (authentication != null + && !StringUtils.isEmpty(authentication.getUsername()) + && !StringUtils.isEmpty(authentication.getPassword())) { + final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials(authentication.getUsername(), authentication.getPassword()) + ); + + clientBuilder + .setHttpClientConfigCallback( + httpClientBuilder -> httpClientBuilder + .setDefaultCredentialsProvider(credentialsProvider) + ); + } + + if (!CollectionUtils.isEmpty(datasourceConfiguration.getHeaders())) { + clientBuilder.setDefaultHeaders( + (Header[]) datasourceConfiguration.getHeaders() + .stream() + .map(h -> new BasicHeader(h.getKey(), h.getValue())) + .toArray() + ); + } + + return Mono.just(clientBuilder.build()); + } + + @Override + public void datasourceDestroy(RestClient client) { + try { + client.close(); + } catch (IOException e) { + log.warn("Error closing connection to ElasticSearch.", e); + } + } + + @Override + public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + + if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + invalids.add("No endpoint provided. Please provide a host:port where ElasticSearch is reachable."); + } + + return invalids; + } + + @Override + public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { + return datasourceCreate(datasourceConfiguration) + .map(client -> { + if (client == null) { + return new DatasourceTestResult("Null client object to ElasticSearch."); + } + + // This HEAD request is to check if an index exists. It response with 200 if the index exists, + // 404 if it doesn't. We just check for either of these two. + // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-exists.html + Request request = new Request("HEAD", "/potentially-missing-index?local=true"); + + final Response response; + try { + response = client.performRequest(request); + } catch (IOException e) { + return new DatasourceTestResult("Error running HEAD request: " + e.getMessage()); + } + + final StatusLine statusLine = response.getStatusLine(); + + try { + client.close(); + } catch (IOException e) { + log.warn("Error closing ElasticSearch client that was made for testing.", e); + } + + if (statusLine.getStatusCode() != 404 && statusLine.getStatusCode() != 200) { + return new DatasourceTestResult( + "Unexpected response from ElasticSearch: " + statusLine); + } + + return new DatasourceTestResult(); + }) + .onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage()))); + } + } +} diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/editor.json new file mode 100644 index 00000000000..ee3cfc1645c --- /dev/null +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/editor.json @@ -0,0 +1,45 @@ +{ + "editor": [ + { + "sectionName": "", + "id": 1, + "children": [ + { + "label": "Method", + "configProperty": "actionConfiguration.httpMethod", + "controlType": "DROP_DOWN", + "isRequired": true, + "initialValue": "GET", + "options": [ + { + "label": "GET", + "value": "GET" + }, + { + "label": "POST", + "value": "POST" + }, + { + "label": "PUT", + "value": "PUT" + }, + { + "label": "DELETE", + "value": "DELETE" + } + ] + }, + { + "label": "Path", + "configProperty": "actionConfiguration.path", + "controlType": "INPUT_TEXT" + }, + { + "label": "Body", + "configProperty": "actionConfiguration.body", + "controlType": "QUERY_DYNAMIC_TEXT" + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/form.json new file mode 100644 index 00000000000..e9f62cc4e37 --- /dev/null +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/form.json @@ -0,0 +1,53 @@ +{ + "form": [ + { + "sectionName": "Connection", + "children": [ + { + "sectionName": null, + "children": [ + { + "label": "Host Address", + "configProperty": "datasourceConfiguration.endpoints[*].host", + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" + }, + { + "label": "Port", + "configProperty": "datasourceConfiguration.endpoints[*].port", + "dataType": "NUMBER", + "controlType": "KEYVALUE_ARRAY" + } + ] + } + ] + }, + { + "sectionName": "Authentication", + "children": [ + { + "label": "Username for Basic Auth", + "configProperty": "datasourceConfiguration.authentication.username", + "controlType": "INPUT_TEXT", + "placeholderText": "Username" + }, + { + "label": "Password for Basic Auth", + "configProperty": "datasourceConfiguration.authentication.password", + "dataType": "PASSWORD", + "controlType": "INPUT_TEXT", + "placeholderText": "Password", + "encrypted": true + }, + { + "label": "Authorization Header (if username, password are not set)", + "configProperty": "datasourceConfiguration.headers[0]", + "controlType": "FIXED_KEY_INPUT", + "fixedKey": "Authorization", + "placeholderText": "Authorization Header" + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java b/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java new file mode 100644 index 00000000000..921c09150f9 --- /dev/null +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java @@ -0,0 +1,206 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.Endpoint; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RestClient; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Slf4j +public class ElasticSearchPluginTest { + ElasticSearchPlugin.ElasticSearchPluginExecutor pluginExecutor = new ElasticSearchPlugin.ElasticSearchPluginExecutor(); + + @ClassRule + public static final ElasticsearchContainer container = + new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:6.4.1") + .withEnv("discovery.type", "single-node"); + + private static final DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + + @BeforeClass + public static void setUp() throws IOException { + final Integer port = container.getMappedPort(9200); + + final RestClient client = RestClient.builder( + new HttpHost("localhost", port, "http") + ).build(); + + Request request; + + request = new Request("PUT", "/planets/doc/id1"); + request.setJsonEntity("{\"name\": \"Mercury\"}"); + client.performRequest(request); + + request = new Request("PUT", "/planets/doc/id2"); + request.setJsonEntity("{\"name\": \"Venus\"}"); + client.performRequest(request); + + request = new Request("PUT", "/planets/doc/id3"); + request.setJsonEntity("{\"name\": \"Earth\"}"); + client.performRequest(request); + + client.close(); + + dsConfig.setEndpoints(List.of(new Endpoint("localhost", port.longValue()))); + } + + private Mono execute(HttpMethod method, String path, String body) { + final ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(method); + actionConfiguration.setPath(path); + actionConfiguration.setBody(body); + + return pluginExecutor + .datasourceCreate(dsConfig) + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + } + + @Test + public void testGet() { + StepVerifier.create(execute(HttpMethod.GET, "/planets/doc/id1", null)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final Map resultBody = (Map) result.getBody(); + assertEquals("Mercury", ((Map) resultBody.get("_source")).get("name")); + }) + .verifyComplete(); + } + + @Test + public void testMultiGet() { + final String contentJson = "{\n" + + " \"docs\": [\n" + + " {\n" + + " \"_index\": \"planets\",\n" + + " \"_id\": \"id1\"\n" + + " },\n" + + " {\n" + + " \"_index\": \"planets\",\n" + + " \"_id\": \"id2\"\n" + + " }\n" + + " ]\n" + + "}"; + StepVerifier.create(execute(HttpMethod.GET, "/planets/_mget", contentJson)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final List docs = ((Map>) result.getBody()).get("docs"); + assertEquals(2, docs.size()); + }) + .verifyComplete(); + } + + @Test + public void testPutCreate() { + final String contentJson = "{\"name\": \"Pluto\"}"; + StepVerifier.create(execute(HttpMethod.PUT, "/planets/doc/id9", contentJson)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final Map resultBody = (Map) result.getBody(); + assertEquals("created", resultBody.get("result")); + assertEquals("id9", resultBody.get("_id")); + }) + .verifyComplete(); + } + + @Test + public void testPutUpdate() { + final String contentJson = "{\"name\": \"New Venus\"}"; + StepVerifier.create(execute(HttpMethod.PUT, "/planets/doc/id2", contentJson)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final Map resultBody = (Map) result.getBody(); + assertEquals("updated", resultBody.get("result")); + assertEquals("id2", resultBody.get("_id")); + }) + .verifyComplete(); + } + + @Test + public void testDelete() { + StepVerifier.create(execute(HttpMethod.DELETE, "/planets/doc/id3", null)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final Map resultBody = (Map) result.getBody(); + assertEquals("deleted", resultBody.get("result")); + assertEquals("id3", resultBody.get("_id")); + }) + .verifyComplete(); + } + + @Test + public void testBulkWithArrayBody() { + final String contentJson = "[\n" + + " { \"index\" : { \"_index\" : \"test1\", \"_type\": \"doc\", \"_id\" : \"1\" } },\n" + + " { \"field1\" : \"value1\" },\n" + + " { \"delete\" : { \"_index\" : \"test1\", \"_type\": \"doc\", \"_id\" : \"2\" } },\n" + + " { \"create\" : { \"_index\" : \"test1\", \"_type\": \"doc\", \"_id\" : \"3\" } },\n" + + " { \"field1\" : \"value3\" },\n" + + " { \"update\" : {\"_id\" : \"1\", \"_type\": \"doc\", \"_index\" : \"test1\"} },\n" + + " { \"doc\" : {\"field2\" : \"value2\"} }\n" + + "]"; + + StepVerifier.create(execute(HttpMethod.POST, "/_bulk", contentJson)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final Map resultBody = (Map) result.getBody(); + assertFalse((Boolean) resultBody.get("errors")); + assertEquals(4, ((List) resultBody.get("items")).size()); + }) + .verifyComplete(); + } + + @Test + public void testBulkWithDirectBody() { + final String contentJson = + "{ \"index\" : { \"_index\" : \"test2\", \"_type\": \"doc\", \"_id\" : \"1\" } }\n" + + "{ \"field1\" : \"value1\" }\n" + + "{ \"delete\" : { \"_index\" : \"test2\", \"_type\": \"doc\", \"_id\" : \"2\" } }\n" + + "{ \"create\" : { \"_index\" : \"test2\", \"_type\": \"doc\", \"_id\" : \"3\" } }\n" + + "{ \"field1\" : \"value3\" }\n" + + "{ \"update\" : {\"_id\" : \"1\", \"_type\": \"doc\", \"_index\" : \"test2\"} }\n" + + "{ \"doc\" : {\"field2\" : \"value2\"} }\n"; + + StepVerifier.create(execute(HttpMethod.POST, "/_bulk", contentJson)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + final Map resultBody = (Map) result.getBody(); + assertFalse((Boolean) resultBody.get("errors")); + assertEquals(4, ((List) resultBody.get("items")).size()); + }) + .verifyComplete(); + } + +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json index 0cd153e6657..a6f4eb44eb6 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json @@ -105,7 +105,8 @@ "configProperty": "datasourceConfiguration.authentication.password", "dataType": "PASSWORD", "controlType": "INPUT_TEXT", - "placeholderText": "Password" + "placeholderText": "Password", + "encrypted": true } ] } diff --git a/app/server/appsmith-plugins/mssqlPlugin/dependency-reduced-pom.xml b/app/server/appsmith-plugins/mssqlPlugin/dependency-reduced-pom.xml new file mode 100644 index 00000000000..f62f7e0f968 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/dependency-reduced-pom.xml @@ -0,0 +1,171 @@ + + + 4.0.0 + com.external.plugins + mssqlPlugin + mssqlPlugin + 1.0-SNAPSHOT + + + + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + false + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib + + + + + + + + + org.pf4j + pf4j-spring + 0.6.0 + provided + + + com.appsmith + interfaces + 1.0-SNAPSHOT + provided + + + org.projectlombok + lombok + 1.18.8 + provided + + + junit + junit + 4.13.1 + test + + + hamcrest-core + org.hamcrest + + + + + org.testcontainers + testcontainers + 1.15.0-rc2 + test + + + commons-compress + org.apache.commons + + + duct-tape + org.rnorth.duct-tape + + + visible-assertions + org.rnorth.visible-assertions + + + docker-java-api + com.github.docker-java + + + docker-java-transport-zerodep + com.github.docker-java + + + + + org.testcontainers + mssqlserver + 1.15.0-rc2 + test + + + jdbc + org.testcontainers + + + + + io.projectreactor + reactor-test + 3.2.11.RELEASE + test + + + org.mockito + mockito-core + 3.1.0 + test + + + byte-buddy + net.bytebuddy + + + byte-buddy-agent + net.bytebuddy + + + objenesis + org.objenesis + + + + + + mssql-plugin + ${java.version} + 11 + ${java.version} + com.external.plugins.MssqlPlugin + UTF-8 + 1.0-SNAPSHOT + tech@appsmith.com + + diff --git a/app/server/appsmith-plugins/mssqlPlugin/plugin.properties b/app/server/appsmith-plugins/mssqlPlugin/plugin.properties new file mode 100644 index 00000000000..571e1d3e4dd --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=mssql-plugin +plugin.class=com.external.plugins.MssqlPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= diff --git a/app/server/appsmith-plugins/mssqlPlugin/pom.xml b/app/server/appsmith-plugins/mssqlPlugin/pom.xml new file mode 100644 index 00000000000..fedeaa4eb7c --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/pom.xml @@ -0,0 +1,146 @@ + + + + 4.0.0 + + com.external.plugins + mssqlPlugin + 1.0-SNAPSHOT + + mssqlPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + mssql-plugin + com.external.plugins.MssqlPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + + org.pf4j + pf4j-spring + 0.6.0 + provided + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + provided + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + com.microsoft.sqlserver + mssql-jdbc + 8.4.1.jre11 + + + + + junit + junit + 4.13.1 + test + + + + org.testcontainers + testcontainers + 1.15.0-rc2 + test + + + org.testcontainers + mssqlserver + 1.15.0-rc2 + test + + + + io.projectreactor + reactor-test + 3.2.11.RELEASE + test + + + org.mockito + mockito-core + 3.1.0 + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib + + + + + + + + diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java new file mode 100644 index 00000000000..ea9f4ecd3b4 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java @@ -0,0 +1,307 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.SSLDetails; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.pluginExceptions.StaleConnectionException; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.ObjectUtils; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.appsmith.external.models.Connection.Mode.READ_ONLY; + +public class MssqlPlugin extends BasePlugin { + + private static final String JDBC_DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + + private static final int VALIDITY_CHECK_TIMEOUT = 5; + + private static final String DATE_COLUMN_TYPE_NAME = "date"; + + public MssqlPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + /** + * MsSQL plugin receives the query as json of the following format : + */ + + @Slf4j + @Extension + public static class MssqlPluginExecutor implements PluginExecutor { + + @Override + public Mono execute(Connection connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + + try { + if (connection == null || connection.isClosed() || !connection.isValid(VALIDITY_CHECK_TIMEOUT)) { + log.info("Encountered stale connection in MsSQL plugin. Reporting back."); + throw new StaleConnectionException(); + } + } catch (SQLException error) { + // This exception is thrown only when the timeout to `isValid` is negative. Since, that's not the case, + // here, this should never happen. + log.error("Error checking validity of MsSQL connection.", error); + } + + String query = actionConfiguration.getBody(); + + if (query == null) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Missing required parameter: Query.")); + } + + List> rowsList = new ArrayList<>(50); + + Statement statement = null; + ResultSet resultSet = null; + try { + statement = connection.createStatement(); + boolean isResultSet = statement.execute(query); + + if (isResultSet) { + resultSet = statement.getResultSet(); + ResultSetMetaData metaData = resultSet.getMetaData(); + int colCount = metaData.getColumnCount(); + + while (resultSet.next()) { + // Use `LinkedHashMap` here so that the column ordering is preserved in the response. + Map row = new LinkedHashMap<>(colCount); + + for (int i = 1; i <= colCount; i++) { + Object value; + final String typeName = metaData.getColumnTypeName(i); + + if (resultSet.getObject(i) == null) { + value = null; + + } else if (DATE_COLUMN_TYPE_NAME.equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE.format(resultSet.getDate(i).toLocalDate()); + + } else if ("timestamp".equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE_TIME.format( + LocalDateTime.of( + resultSet.getDate(i).toLocalDate(), + resultSet.getTime(i).toLocalTime() + ) + ) + "Z"; + + } else if ("timestamptz".equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE_TIME.format( + resultSet.getObject(i, OffsetDateTime.class) + ); + + } else if ("time".equalsIgnoreCase(typeName) || "timetz".equalsIgnoreCase(typeName)) { + value = resultSet.getString(i); + + } else if ("interval".equalsIgnoreCase(typeName)) { + value = resultSet.getObject(i).toString(); + + } else { + value = resultSet.getObject(i); + + } + + row.put(metaData.getColumnName(i), value); + } + + rowsList.add(row); + } + + } else { + rowsList.add(Map.of( + "affectedRows", + ObjectUtils.defaultIfNull(statement.getUpdateCount(), 0)) + ); + + } + + } catch (SQLException e) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage())); + + } finally { + if (resultSet != null) { + try { + resultSet.close(); + } catch (SQLException e) { + log.warn("Error closing MsSQL ResultSet", e); + } + } + + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + log.warn("Error closing MsSQL Statement", e); + } + } + + } + + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(objectMapper.valueToTree(rowsList)); + result.setIsExecutionSuccess(true); + log.debug("In the MssqlPlugin, got action execution result: " + result.toString()); + return Mono.just(result); + } + + @Override + public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + try { + Class.forName(JDBC_DRIVER); + } catch (ClassNotFoundException e) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Error loading MsSQL JDBC Driver class." + )); + } + + AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + + com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection(); + + final boolean isSslEnabled = configurationConnection != null + && configurationConnection.getSsl() != null + && !SSLDetails.AuthType.NO_SSL.equals(configurationConnection.getSsl().getAuthType()); + + StringBuilder urlBuilder = new StringBuilder("jdbc:sqlserver://"); + for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + urlBuilder + .append(endpoint.getHost()) + .append(":") + .append(ObjectUtils.defaultIfNull(endpoint.getPort(), 5432L)) + .append(";"); + } + + if (!StringUtils.isEmpty(authentication.getDatabaseName())) { + urlBuilder + .append("database=") + .append(authentication.getDatabaseName()) + .append(";"); + } + + if (!StringUtils.isEmpty(authentication.getUsername())) { + urlBuilder + .append("user=") + .append(authentication.getUsername()) + .append(";"); + } + + if (!StringUtils.isEmpty(authentication.getPassword())) { + urlBuilder + .append("password=") + .append(authentication.getPassword()) + .append(";"); + } + + urlBuilder + .append("encrypt=") + .append(isSslEnabled) + .append(";"); + + try { + Connection connection = DriverManager.getConnection(urlBuilder.toString()); + connection.setReadOnly( + configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode())); + return Mono.just(connection); + + } catch (SQLException e) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Error connecting to MsSQL: " + e.getMessage() + )); + + } + } + + @Override + public void datasourceDestroy(Connection connection) { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + log.error("Error closing MsSQL Connection.", e); + } + } + + @Override + public Set validateDatasource(@NonNull DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + + if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + invalids.add("Missing endpoint."); + } + + if (datasourceConfiguration.getConnection() != null + && datasourceConfiguration.getConnection().getMode() == null) { + invalids.add("Missing Connection Mode."); + } + + if (datasourceConfiguration.getAuthentication() == null) { + invalids.add("Missing authentication details."); + + } else { + if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getUsername())) { + invalids.add("Missing username for authentication."); + } + + if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getPassword())) { + invalids.add("Missing password for authentication."); + } + + } + + return invalids; + } + + @Override + public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { + return datasourceCreate(datasourceConfiguration) + .map(connection -> { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + log.warn("Error closing MsSQL connection that was made for testing.", e); + } + + return new DatasourceTestResult(); + }) + .onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage()))); + } + + } + +} diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/editor.json new file mode 100644 index 00000000000..7896a10ac66 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/editor.json @@ -0,0 +1,15 @@ +{ + "editor": [ + { + "sectionName": "", + "id": 1, + "children": [ + { + "label": "", + "configProperty": "actionConfiguration.body", + "controlType": "QUERY_DYNAMIC_TEXT" + } + ] + } + ] +} \ No newline at end of file diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/form.json new file mode 100644 index 00000000000..6df259b8c75 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/form.json @@ -0,0 +1,154 @@ +{ + "form": [ + { + "sectionName": "Connection", + "id": 1, + "children": [ + { + "label": "Connection Mode", + "configProperty": "datasourceConfiguration.connection.mode", + "controlType": "DROP_DOWN", + "isRequired": true, + "initialValue": "READ_WRITE", + "options": [ + { + "label": "Read Only", + "value": "READ_ONLY" + }, + { + "label": "Read / Write", + "value": "READ_WRITE" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "Host Address", + "configProperty": "datasourceConfiguration.endpoints[*].host", + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" + }, + { + "label": "Port", + "configProperty": "datasourceConfiguration.endpoints[*].port", + "dataType": "NUMBER", + "controlType": "KEYVALUE_ARRAY" + } + ] + }, + { + "label": "Database Name", + "configProperty": "datasourceConfiguration.authentication.databaseName", + "controlType": "INPUT_TEXT", + "placeholderText": "Database name", + "initialValue": "admin" + } + ] + }, + { + "sectionName": "Authentication", + "id": 2, + "children": [ + { + "sectionName": null, + "children": [ + { + "label": "Username", + "configProperty": "datasourceConfiguration.authentication.username", + "controlType": "INPUT_TEXT", + "placeholderText": "Username" + }, + { + "label": "Password", + "configProperty": "datasourceConfiguration.authentication.password", + "dataType": "PASSWORD", + "controlType": "INPUT_TEXT", + "placeholderText": "Password" + } + ] + } + ] + }, + { + "id": 3, + "sectionName": "SSL (optional)", + "children": [ + { + "label": "SSL Mode", + "configProperty": "datasourceConfiguration.connection.ssl.authType", + "controlType": "DROP_DOWN", + "options": [ + { + "label": "No SSL", + "value": "NO_SSL" + }, + { + "label": "Allow", + "value": "ALLOW" + }, + { + "label": "Prefer", + "value": "PREFER" + }, + { + "label": "Require", + "value": "REQUIRE" + }, + { + "label": "Disable", + "value": "DISABLE" + }, + { + "label": "Verify-CA", + "value": "VERIFY_CA" + }, + { + "label": "Verify-Full", + "value": "VERIFY_FULL" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "Key File", + "configProperty": "datasourceConfiguration.connection.ssl.keyFile", + "controlType": "FILE_PICKER" + }, + { + "label": "Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.certificateFile", + "controlType": "FILE_PICKER" + } + ] + }, + { + "sectionName": null, + "children": [ + { + "label": "CA Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.caCertificateFile", + "controlType": "FILE_PICKER" + }, + { + "label": "PEM Certificate", + "configProperty": "datasourceConfiguration.connection.ssl.pemCertificate.file", + "controlType": "FILE_PICKER" + }, + { + "label": "PEM Passphrase", + "configProperty": "datasourceConfiguration.connection.ssl.pemCertificate.password", + "dataType": "PASSWORD", + "controlType": "INPUT_TEXT", + "placeholderText": "PEM Passphrase" + } + ] + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/CREATE.sql b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/CREATE.sql new file mode 100644 index 00000000000..60948f8b63c --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/CREATE.sql @@ -0,0 +1,8 @@ +INSERT INTO users + (name, gender, email) +VALUES + ( + '{{ nameInput.text }}', + '{{ genderDropdown.selectedOptionValue }}', + '{{ nameInput.text }}' + ); diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/DELETE.sql b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/DELETE.sql new file mode 100644 index 00000000000..aad8425f9b0 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/DELETE.sql @@ -0,0 +1 @@ +DELETE FROM users WHERE id = '{{ usersTable.selectedRow.id }}'; diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/SELECT.sql b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/SELECT.sql new file mode 100644 index 00000000000..80ef4f21340 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/SELECT.sql @@ -0,0 +1 @@ +SELECT * FROM users where role = '{{ roleDropdown.selectedOptionValue }}' ORDER BY id LIMIT 10; \ No newline at end of file diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/UPDATE.sql b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/UPDATE.sql new file mode 100644 index 00000000000..4c1b31b32be --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/UPDATE.sql @@ -0,0 +1,3 @@ +UPDATE users + SET status = 'APPROVED' + WHERE id = '{{ usersTable.selectedRow.id }}'; diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java new file mode 100644 index 00000000000..fa4bcf13658 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java @@ -0,0 +1,204 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.Endpoint; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.containers.MSSQLServerContainer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.LinkedHashMap; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for the PostgresPlugin + */ +@Slf4j +public class MssqlPluginTest { + + MssqlPlugin.MssqlPluginExecutor pluginExecutor = new MssqlPlugin.MssqlPluginExecutor(); + + @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional. + @ClassRule + public static final MSSQLServerContainer container = + new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2017-latest") + .acceptLicense() + .withExposedPorts(1433) + .withPassword("Mssql123"); + + private static String address; + private static Integer port; + private static String username, password; + + @BeforeClass + public static void setUp() throws SQLException { + address = container.getContainerIpAddress(); + port = container.getMappedPort(1433); + username = container.getUsername(); + password = container.getPassword(); + + try (Connection connection = DriverManager.getConnection( + "jdbc:sqlserver://" + address + ":" + port + ";user=" + username + ";password=" + password + )) { + + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE users (\n" + + " id int identity (1, 1) NOT NULL,\n" + + " username VARCHAR (50) UNIQUE NOT NULL,\n" + + " password VARCHAR (50) NOT NULL,\n" + + " email VARCHAR (355) UNIQUE NOT NULL,\n" + + " spouse_dob DATE,\n" + + " dob DATE NOT NULL,\n" + + " time1 TIME NOT NULL,\n" + + " constraint pk_users_id primary key (id)\n" + + ")"); + + statement.execute("CREATE TABLE possessions (\n" + + " id int identity (1, 1) not null,\n" + + " title VARCHAR (50) NOT NULL,\n" + + " user_id int NOT NULL,\n" + + " constraint pk_possessions_id primary key (id),\n" + + " constraint user_fk foreign key (user_id) references users(id)\n" + + ")"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute("SET identity_insert users ON;"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute( + "INSERT INTO users (id, username, password, email, spouse_dob, dob, time1) VALUES (" + + "1, 'Jack', 'jill', 'jack@exemplars.com', NULL, '2018-12-31'," + + " '18:32:45'" + + ")"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute( + "INSERT INTO users (id, username, password, email, spouse_dob, dob, time1) VALUES (" + + "2, 'Jill', 'jack', 'jill@exemplars.com', NULL, '2019-12-31'," + + " '15:45:30'" + + ")"); + } + + } + } + + private DatasourceConfiguration createDatasourceConfiguration() { + AuthenticationDTO authDTO = new AuthenticationDTO(); + authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + authDTO.setUsername(username); + authDTO.setPassword(password); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost(address); + endpoint.setPort(port.longValue()); + + DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + dsConfig.setAuthentication(authDTO); + dsConfig.setEndpoints(List.of(endpoint)); + return dsConfig; + } + + @Test + public void testConnectPostgresContainer() { + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + StepVerifier.create(dsConnectionMono) + .assertNext(Assert::assertNotNull) + .verifyComplete(); + } + + @Test + public void testAliasColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "user_id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + }) + .verifyComplete(); + } + + @Test + public void testExecute() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("18:32:45.0000000", node.get("time1").asText()); + assertTrue(node.get("spouse_dob").isNull()); + + // Check the order of the columns. + assertArrayEquals( + new String[]{ + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "time1", + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + }) + .verifyComplete(); + } + +} diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json index 5bbe87017ff..ec5dba15bba 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json @@ -66,7 +66,8 @@ "configProperty": "datasourceConfiguration.authentication.password", "dataType": "PASSWORD", "controlType": "INPUT_TEXT", - "placeholderText": "Password" + "placeholderText": "Password", + "encrypted": true } ] } diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index 83f2d5f1bd7..3e2cedb946e 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -9,7 +9,6 @@ 4.0.0 - com.appsmith appsmith-plugins 1.0-SNAPSHOT pom @@ -20,6 +19,9 @@ mongoPlugin rapidApiPlugin mysqlPlugin + elasticSearchPlugin + dynamoPlugin + redisPlugin + mssqlPlugin - - + \ No newline at end of file diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json index 6df259b8c75..eb58eb62e85 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json @@ -66,7 +66,8 @@ "configProperty": "datasourceConfiguration.authentication.password", "dataType": "PASSWORD", "controlType": "INPUT_TEXT", - "placeholderText": "Password" + "placeholderText": "Password", + "encrypted": true } ] } diff --git a/app/server/appsmith-plugins/redisPlugin/plugin.properties b/app/server/appsmith-plugins/redisPlugin/plugin.properties new file mode 100644 index 00000000000..382cc2fbe9e --- /dev/null +++ b/app/server/appsmith-plugins/redisPlugin/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=redis-plugin +plugin.class=com.external.plugins.RedisPlugin +plugin.version=1.0-SNAPSHOT +plugin.provider=tech@appsmith.com +plugin.dependencies= \ No newline at end of file diff --git a/app/server/appsmith-plugins/redisPlugin/pom.xml b/app/server/appsmith-plugins/redisPlugin/pom.xml new file mode 100644 index 00000000000..f41de124e4a --- /dev/null +++ b/app/server/appsmith-plugins/redisPlugin/pom.xml @@ -0,0 +1,139 @@ + + + + 4.0.0 + + com.external.plugins + redisPlugin + 1.0-SNAPSHOT + + redisPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + redis-plugin + com.external.plugins.RedisPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + + org.pf4j + pf4j-spring + 0.6.0 + provided + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + provided + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + redis.clients + jedis + 3.3.0 + + + org.slf4j + slf4j-api + + + + + + + + junit + junit + 4.13.1 + test + + + + io.projectreactor + reactor-test + 3.2.11.RELEASE + test + + + org.mockito + mockito-core + 3.1.0 + test + + + org.testcontainers + testcontainers + 1.15.0-rc2 + test + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + + + + + + + package + + shade + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + runtime + ${project.build.directory}/lib + + + + + + + + + diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java new file mode 100644 index 00000000000..fadee2dc182 --- /dev/null +++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java @@ -0,0 +1,178 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.ObjectUtils; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import org.pf4j.util.StringUtils; +import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Mono; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.exceptions.JedisConnectionException; +import redis.clients.jedis.util.SafeEncoder; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class RedisPlugin extends BasePlugin { + private static final Integer DEFAULT_PORT = 6379; + + public RedisPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Slf4j + @Extension + public static class RedisPluginExecutor implements PluginExecutor { + @Override + public Mono execute(Jedis jedis, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + String body = actionConfiguration.getBody(); + if (StringUtils.isNullOrEmpty(body)) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, + String.format("Body is null or empty [%s]", body))); + } + + // First value will be the redis command and others are arguments for that command + String[] bodySplitted = body.trim().split("\\s+"); + + Protocol.Command command; + try { + // Commands are in upper case + command = Protocol.Command.valueOf(bodySplitted[0].toUpperCase()); + } catch (IllegalArgumentException exc) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, + String.format("Not a valid Redis command:%s", bodySplitted[0]))); + } + + Object commandOutput; + if (bodySplitted.length > 1) { + commandOutput = jedis.sendCommand(command, Arrays.copyOfRange(bodySplitted, 1, bodySplitted.length)); + } else { + commandOutput = jedis.sendCommand(command); + } + + ActionExecutionResult actionExecutionResult = new ActionExecutionResult(); + actionExecutionResult.setBody(objectMapper.valueToTree(processCommandOutput(commandOutput))); + actionExecutionResult.setIsExecutionSuccess(true); + + return Mono.just(actionExecutionResult); + } + + // This will be updated as we encounter different outputs. + private List> processCommandOutput(Object commandOutput) { + if (commandOutput == null) { + return List.of(Map.of("result", "null")); + } else if (commandOutput instanceof byte[]) { + return List.of(Map.of("result", SafeEncoder.encode((byte[]) commandOutput))); + } else if (commandOutput instanceof List) { + List commandList = (List) commandOutput; + return commandList.stream() + .map(obj -> Map.of("result", SafeEncoder.encode(obj))) + .collect(Collectors.toList()); + } else { + return List.of(Map.of("result", String.valueOf(commandOutput))); + } + } + + @Override + public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + if (datasourceConfiguration.getEndpoints().isEmpty()) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "No endpoint(s) configured")); + } + + Endpoint endpoint = datasourceConfiguration.getEndpoints().get(0); + Integer port = (int) (long) ObjectUtils.defaultIfNull(endpoint.getPort(), DEFAULT_PORT); + Jedis jedis = new Jedis(endpoint.getHost(), port); + + AuthenticationDTO auth = datasourceConfiguration.getAuthentication(); + if (auth != null && AuthenticationDTO.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) { + jedis.auth(auth.getUsername(), auth.getPassword()); + } + + return Mono.just(jedis); + } + + @Override + public void datasourceDestroy(Jedis jedis) { + try { + if (jedis != null) { + jedis.close(); + } + } catch (JedisConnectionException exc) { + log.error("Error closing Redis connection"); + } + } + + @Override + public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { + Set invalids = new HashSet<>(); + + if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + invalids.add("Missing endpoint(s)"); + } else { + Endpoint endpoint = datasourceConfiguration.getEndpoints().get(0); + if (StringUtils.isNullOrEmpty(endpoint.getHost())) { + invalids.add("Missing host for endpoint"); + } + } + + AuthenticationDTO auth = datasourceConfiguration.getAuthentication(); + if (auth != null && AuthenticationDTO.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) { + if (StringUtils.isNullOrEmpty(datasourceConfiguration.getAuthentication().getUsername())) { + invalids.add("Missing username for authentication."); + } + + if (StringUtils.isNullOrEmpty(datasourceConfiguration.getAuthentication().getPassword())) { + invalids.add("Missing password for authentication."); + } + } + + return invalids; + } + + private Mono verifyPing(Jedis jedis) { + String pingResponse; + try { + pingResponse = jedis.ping(); + } catch (Exception exc) { + return Mono.error(exc); + } + + if (!"PONG".equals(pingResponse)) { + return Mono.error(new RuntimeException( + String.format("Expected PONG in response of PING but got %s", pingResponse))); + } + + return Mono.empty(); + } + + @Override + public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { + return datasourceCreate(datasourceConfiguration) + .map(jedis -> { + verifyPing(jedis).block(); + datasourceDestroy(jedis); + return new DatasourceTestResult(); + }) + .onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage()))); + } + + } +} diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/redisPlugin/src/main/resources/editor.json new file mode 100644 index 00000000000..7896a10ac66 --- /dev/null +++ b/app/server/appsmith-plugins/redisPlugin/src/main/resources/editor.json @@ -0,0 +1,15 @@ +{ + "editor": [ + { + "sectionName": "", + "id": 1, + "children": [ + { + "label": "", + "configProperty": "actionConfiguration.body", + "controlType": "QUERY_DYNAMIC_TEXT" + } + ] + } + ] +} \ No newline at end of file diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/redisPlugin/src/main/resources/form.json new file mode 100644 index 00000000000..c0a06c5e24c --- /dev/null +++ b/app/server/appsmith-plugins/redisPlugin/src/main/resources/form.json @@ -0,0 +1,53 @@ +{ + "form": [ + { + "sectionName": "Connection", + "id": 1, + "children": [ + { + "sectionName": null, + "children": [ + { + "label": "Host Address", + "configProperty": "datasourceConfiguration.endpoints[*].host", + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" + }, + { + "label": "Port", + "configProperty": "datasourceConfiguration.endpoints[*].port", + "dataType": "NUMBER", + "controlType": "KEYVALUE_ARRAY" + } + ] + } + ] + }, + { + "sectionName": "Authentication", + "id": 2, + "children": [ + { + "sectionName": null, + "children": [ + { + "label": "Username", + "configProperty": "datasourceConfiguration.authentication.username", + "controlType": "INPUT_TEXT", + "placeholderText": "Username" + }, + { + "label": "Password", + "configProperty": "datasourceConfiguration.authentication.password", + "dataType": "PASSWORD", + "controlType": "INPUT_TEXT", + "placeholderText": "Password", + "encrypted": true + } + ] + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java new file mode 100644 index 00000000000..9728700a9ee --- /dev/null +++ b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java @@ -0,0 +1,223 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import redis.clients.jedis.Jedis; + +import java.util.Collections; +import java.util.Set; + +@Slf4j +public class RedisPluginTest { + @ClassRule + public static final GenericContainer redis = new GenericContainer("redis:5.0.3-alpine") + .withExposedPorts(6379); + private static String host; + private static Integer port; + + private RedisPlugin.RedisPluginExecutor pluginExecutor = new RedisPlugin.RedisPluginExecutor(); + + @BeforeClass + public static void setup() { + host = redis.getContainerIpAddress(); + port = redis.getFirstMappedPort(); + } + + private DatasourceConfiguration createDatasourceConfiguration() { + Endpoint endpoint = new Endpoint(); + endpoint.setHost(host); + endpoint.setPort(Long.valueOf(port)); + + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + return datasourceConfiguration; + } + + @Test + public void itShouldCreateDatasource() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + + StepVerifier.create(jedisMono) + .assertNext(Assert::assertNotNull) + .verifyComplete(); + + pluginExecutor.datasourceDestroy(jedisMono.block()); + } + + @Test + public void itShouldValidateDatasourceWithNoEndpoints() { + DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration(); + + Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), Set.of("Missing endpoint(s)")); + } + + @Test + public void itShouldValidateDatasourceWithInvalidEndpoint() { + DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration(); + + Endpoint endpoint = new Endpoint(); + invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), Set.of("Missing host for endpoint")); + } + + @Test + public void itShouldValidateDatasourceWithInvalidAuth() { + DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration(); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost("test-host"); + + AuthenticationDTO invalidAuth = new AuthenticationDTO(); + invalidAuth.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + + invalidDatasourceConfiguration.setAuthentication(invalidAuth); + invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), + Set.of("Missing username for authentication.", "Missing password for authentication.") + ); + } + + @Test + public void itShouldValidateDatasource() { + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + + AuthenticationDTO auth = new AuthenticationDTO(); + auth.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + auth.setUsername("test-username"); + auth.setPassword("test-password"); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost("test-host"); + + datasourceConfiguration.setAuthentication(auth); + datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + Assert.assertTrue(pluginExecutor.validateDatasource(datasourceConfiguration).isEmpty()); + } + + @Test + public void itShouldTestDatasource() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + Mono datasourceTestResultMono = pluginExecutor.testDatasource(datasourceConfiguration); + + StepVerifier.create(datasourceTestResultMono) + .assertNext(datasourceTestResult -> { + Assert.assertNotNull(datasourceTestResult); + Assert.assertTrue(datasourceTestResult.isSuccess()); + }) + .verifyComplete(); + } + + @Test + public void itShouldThrowErrorIfEmptyBody() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Mono actionExecutionResultMono = jedisMono + .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration)); + + StepVerifier.create(actionExecutionResultMono) + .expectError(AppsmithPluginException.class) + .verify(); + } + + @Test + public void itShouldThrowErrorIfInvalidRedisCommand() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("LOL"); + + Mono actionExecutionResultMono = jedisMono + .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration)); + + StepVerifier.create(actionExecutionResultMono) + .expectError(AppsmithPluginException.class) + .verify(); + } + + @Test + public void itShouldExecuteCommandWithoutArgs() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("PING"); + + Mono actionExecutionResultMono = jedisMono + .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, actionConfiguration)); + + StepVerifier.create(actionExecutionResultMono) + .assertNext(actionExecutionResult -> { + Assert.assertNotNull(actionExecutionResult); + Assert.assertNotNull(actionExecutionResult.getBody()); + final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0); + Assert.assertEquals(node.get("result").asText(), "PONG"); + }).verifyComplete(); + } + + @Test + public void itShouldExecuteCommandWithArgs() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + Mono jedisMono = pluginExecutor.datasourceCreate(datasourceConfiguration); + + // Getting a non-existent key + ActionConfiguration getActionConfiguration = new ActionConfiguration(); + getActionConfiguration.setBody("GET key"); + Mono actionExecutionResultMono = jedisMono + .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, getActionConfiguration)); + StepVerifier.create(actionExecutionResultMono) + .assertNext(actionExecutionResult -> { + Assert.assertNotNull(actionExecutionResult); + Assert.assertNotNull(actionExecutionResult.getBody()); + final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0); + Assert.assertEquals(node.get("result").asText(), "null"); + }).verifyComplete(); + + // Setting a key + ActionConfiguration setActionConfiguration = new ActionConfiguration(); + setActionConfiguration.setBody("SET key value"); + actionExecutionResultMono = jedisMono + .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, setActionConfiguration)); + StepVerifier.create(actionExecutionResultMono) + .assertNext(actionExecutionResult -> { + Assert.assertNotNull(actionExecutionResult); + Assert.assertNotNull(actionExecutionResult.getBody()); + final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0); + Assert.assertEquals(node.get("result").asText(), "OK"); + }).verifyComplete(); + + // Getting the key + actionExecutionResultMono = jedisMono + .flatMap(jedis -> pluginExecutor.execute(jedis, datasourceConfiguration, getActionConfiguration)); + StepVerifier.create(actionExecutionResultMono) + .assertNext(actionExecutionResult -> { + Assert.assertNotNull(actionExecutionResult); + Assert.assertNotNull(actionExecutionResult.getBody()); + final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0); + Assert.assertEquals(node.get("result").asText(), "value"); + }).verifyComplete(); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java index 2c2893f1217..89b05b10d7c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; @@ -51,8 +52,10 @@ public Mono>> getUserMembersOfOrganization(@PathVaria } @PutMapping("/{orgId}/role") - public Mono> updateRoleForMember(@RequestBody UserRole updatedUserRole, @PathVariable String orgId) { - return userOrganizationService.updateRoleForMember(orgId, updatedUserRole) + public Mono> updateRoleForMember(@RequestBody UserRole updatedUserRole, + @PathVariable String orgId, + @RequestHeader(name = "Origin", required = false) String originHeader) { + return userOrganizationService.updateRoleForMember(orgId, updatedUserRole, originHeader) .map(user -> new ResponseDTO<>(HttpStatus.OK.value(), user, null)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java index 856884a2986..7777d9715c8 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java @@ -21,6 +21,7 @@ import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.QApplication; +import com.appsmith.server.domains.QPlugin; import com.appsmith.server.domains.Query; import com.appsmith.server.domains.Role; import com.appsmith.server.domains.Sequence; @@ -532,20 +533,7 @@ public void mysqlPlugin(MongoTemplate mongoTemplate) { log.warn("mysql-plugin already present in database."); } - for (Organization organization : mongoTemplate.findAll(Organization.class)) { - if (CollectionUtils.isEmpty(organization.getPlugins())) { - organization.setPlugins(new ArrayList<>()); - } - - final Set installedPlugins = organization.getPlugins() - .stream().map(OrganizationPlugin::getPluginId).collect(Collectors.toSet()); - - if (!installedPlugins.contains(plugin1.getId())) { - organization.getPlugins() - .add(new OrganizationPlugin(plugin1.getId(), OrganizationPluginStatus.FREE)); - } - mongoTemplate.save(organization); - } + installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); } @ChangeSet(order = "019", id = "update-database-documentation-links", author = "") @@ -936,6 +924,96 @@ public void fixTokenExpiration(MongoTemplate mongoTemplate) { ); } + @ChangeSet(order = "027", id = "add-elastic-search-plugin", author = "") + public void addElasticSearchPlugin(MongoTemplate mongoTemplate) { + Plugin plugin1 = new Plugin(); + plugin1.setName("ElasticSearch"); + plugin1.setType(PluginType.DB); + plugin1.setPackageName("elasticsearch-plugin"); + plugin1.setUiComponent("DbEditorForm"); + plugin1.setResponseType(Plugin.ResponseType.JSON); + plugin1.setIconLocation("https://s3.us-east-2.amazonaws.com/assets.appsmith.com/ElasticSearch.jpg"); + plugin1.setDocumentationLink("https://docs.appsmith.com/core-concepts/connecting-to-databases/querying-elasticsearch"); + plugin1.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin1); + } catch (DuplicateKeyException e) { + log.warn(plugin1.getPackageName() + " already present in database."); + } + + installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); + } + + @ChangeSet(order = "028", id = "add-dynamo-plugin", author = "") + public void addDynamoPlugin(MongoTemplate mongoTemplate) { + Plugin plugin1 = new Plugin(); + plugin1.setName("DynamoDB"); + plugin1.setType(PluginType.DB); + plugin1.setPackageName("dynamo-plugin"); + plugin1.setUiComponent("DbEditorForm"); + plugin1.setResponseType(Plugin.ResponseType.JSON); + plugin1.setIconLocation("https://s3.us-east-2.amazonaws.com/assets.appsmith.com/DynamoDB.png"); + plugin1.setDocumentationLink("https://docs.appsmith.com/core-concepts/connecting-to-databases/querying-dynamodb"); + plugin1.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin1); + } catch (DuplicateKeyException e) { + log.warn(plugin1.getPackageName() + " already present in database."); + } + + installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); + } + + @ChangeSet(order = "029", id = "use-png-logos", author = "") + public void usePngLogos(MongoTemplate mongoTemplate) { + mongoTemplate.updateFirst( + query(where(fieldName(QPlugin.plugin.packageName)).is("elasticsearch-plugin")), + update(fieldName(QPlugin.plugin.iconLocation), + "https://s3.us-east-2.amazonaws.com/assets.appsmith.com/ElasticSearch.png"), + Plugin.class + ); + } + + @ChangeSet(order = "030", id = "add-redis-plugin", author = "") + public void addRedisPlugin(MongoTemplate mongoTemplate) { + Plugin plugin1 = new Plugin(); + plugin1.setName("Redis"); + plugin1.setType(PluginType.DB); + plugin1.setPackageName("redis-plugin"); + plugin1.setUiComponent("DbEditorForm"); + plugin1.setResponseType(Plugin.ResponseType.TABLE); + plugin1.setIconLocation("https://s3.us-east-2.amazonaws.com/assets.appsmith.com/redis.jpg"); + plugin1.setDocumentationLink("https://docs.appsmith.com/core-concepts/connecting-to-databases/querying-redis"); + plugin1.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin1); + } catch (DuplicateKeyException e) { + log.warn(plugin1.getPackageName() + " already present in database."); + } + + installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); + } + + @ChangeSet(order = "030", id = "add-msSql-plugin", author = "") + public void addMsSqlPlugin(MongoTemplate mongoTemplate) { + Plugin plugin1 = new Plugin(); + plugin1.setName("MsSQL"); + plugin1.setType(PluginType.DB); + plugin1.setPackageName("mssql-plugin"); + plugin1.setUiComponent("DbEditorForm"); + plugin1.setResponseType(Plugin.ResponseType.TABLE); + plugin1.setIconLocation("https://s3.us-east-2.amazonaws.com/assets.appsmith.com/MsSQL.jpg"); + plugin1.setDocumentationLink("https://docs.appsmith.com/core-concepts/connecting-to-databases/querying-mssql"); + plugin1.setDefaultInstall(true); + try { + mongoTemplate.insert(plugin1); + } catch (DuplicateKeyException e) { + log.warn(plugin1.getPackageName() + " already present in database."); + } + + installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); + } + private void installPluginToAllOrganizations(MongoTemplate mongoTemplate, String pluginId) { for (Organization organization : mongoTemplate.findAll(Organization.class)) { if (CollectionUtils.isEmpty(organization.getPlugins())) { @@ -954,6 +1032,4 @@ private void installPluginToAllOrganizations(MongoTemplate mongoTemplate, String } } - - } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepository.java index 12307392077..8c04b55885b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepository.java @@ -2,6 +2,7 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.Organization; +import org.springframework.data.domain.Sort; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -11,6 +12,6 @@ public interface CustomOrganizationRepository extends AppsmithRepository findByName(String name, AclPermission aclPermission); - Flux findByIdsIn(Set orgIds, AclPermission aclPermission); + Flux findByIdsIn(Set orgIds, AclPermission aclPermission, Sort sort); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepositoryImpl.java index b0024820bf1..a295a8b87ea 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepositoryImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomOrganizationRepositoryImpl.java @@ -4,6 +4,7 @@ import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.QOrganization; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.query.Criteria; @@ -33,8 +34,9 @@ public Mono findByName(String name, AclPermission aclPermission) { } @Override - public Flux findByIdsIn(Set orgIds, AclPermission aclPermission) { + public Flux findByIdsIn(Set orgIds, AclPermission aclPermission, Sort sort) { Criteria orgIdsCriteria = where(fieldName(QOrganization.organization.id)).in(orgIds); - return queryAll(List.of(orgIdsCriteria), aclPermission); + + return queryAll(List.of(orgIdsCriteria), aclPermission, sort); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java index a9fa811d768..d373ac009be 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.http.codec.multipart.Part; @@ -257,7 +258,9 @@ public Mono findByIdAndPluginsPluginId(String organizationId, Stri @Override public Flux findByIdsIn(Set ids, AclPermission permission) { - return repository.findByIdsIn(ids, permission); + Sort sort = Sort.by(FieldName.NAME); + + return repository.findByIdsIn(ids, permission, sort); } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationService.java index a5fdfee05e7..4e9c8a5f1bd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationService.java @@ -20,7 +20,7 @@ public interface UserOrganizationService { Mono removeUserRoleFromOrganizationGivenUserObject(Organization organization, User user); - Mono updateRoleForMember(String orgId, UserRole userRole); + Mono updateRoleForMember(String orgId, UserRole userRole, String originHeader); Mono bulkAddUsersToOrganization(Organization organization, List users, String roleName); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java index 97957eaf02a..58003ed1b1f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java @@ -248,7 +248,7 @@ public Mono removeUserRoleFromOrganizationGivenUserObject(Organiza } @Override - public Mono updateRoleForMember(String orgId, UserRole userRole) { + public Mono updateRoleForMember(String orgId, UserRole userRole, String originHeader) { if (userRole.getUsername() == null) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "username")); } @@ -293,8 +293,9 @@ public Mono updateRoleForMember(String orgId, UserRole userRole) { Map params = new HashMap<>(); params.put("Inviter_First_Name", currentUser.getName()); params.put("inviter_org_name", organization.getName()); + params.put("inviteUrl", originHeader); params.put("user_role_name", userRole.getRoleName()); - + Mono emailMono = emailSender.sendMail(user.getEmail(), "Appsmith: Your Role has been changed", UPDATE_ROLE_EXISTING_USER_TEMPLATE, params); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ApplicationFetcher.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ApplicationFetcher.java index 3a04bd68f6c..4750c2c9f0d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ApplicationFetcher.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ApplicationFetcher.java @@ -70,33 +70,28 @@ public Mono getAllApplications() { return userMono .flatMap(user -> { Set orgIds = user.getOrganizationIds(); - /* - * For all the organization ids present in the user object, fetch all the organization objects - * and store in a map for fast access. - */ - Mono> organizationsMapMono = organizationService - .findByIdsIn(orgIds, READ_ORGANIZATIONS) - .collectMap(Organization::getId, Function.identity()); + + // Collect all the applications as a map with organization id as a key + + Mono>> applicationsMapMono = applicationRepository + .findByMultipleOrganizationIds(orgIds, READ_APPLICATIONS) + .collectMultimap(Application::getOrganizationId, Function.identity()); UserHomepageDTO userHomepageDTO = new UserHomepageDTO(); userHomepageDTO.setUser(user); - return applicationRepository - // Fetch all the applications which belong the organization ids present in the user - .findByMultipleOrganizationIds(orgIds, READ_APPLICATIONS) - // Collect all the applications as a map with organization id as a key - .collectMultimap(Application::getOrganizationId) - .zipWith(organizationsMapMono) + return organizationService + .findByIdsIn(orgIds, READ_ORGANIZATIONS) + .collectList() + .zipWith(applicationsMapMono) .map(tuple -> { - Map> applicationsCollectionByOrgId = tuple.getT1(); - Map organizationsMap = tuple.getT2(); + List organizations = tuple.getT1(); + Map> applicationsCollectionByOrgId = tuple.getT2(); List organizationApplicationsDTOS = new ArrayList<>(); - for (Map.Entry organizationEntry : organizationsMap.entrySet()) { - String orgId = organizationEntry.getKey(); - Organization organization = organizationEntry.getValue(); - Collection applicationCollection = applicationsCollectionByOrgId.get(orgId); + for (Organization organization : organizations) { + Collection applicationCollection = applicationsCollectionByOrgId.get(organization.getId()); final List applicationList = new ArrayList<>(); if (!CollectionUtils.isEmpty(applicationCollection)) { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java index 43b7c58e81e..6b45d93dd65 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java @@ -326,14 +326,21 @@ public void getAllApplicationsForHome() { //In case of anonymous user, we should have errored out. Assert that the user is not anonymous. assertThat(userHomepageDTO.getUser().getIsAnonymous()).isFalse(); - List organizationApplications = userHomepageDTO.getOrganizationApplications(); + List organizationApplicationsDTOs = userHomepageDTO.getOrganizationApplications(); + + assertThat(organizationApplicationsDTOs.size() > 0); + + for (OrganizationApplicationsDTO organizationApplicationDTO : organizationApplicationsDTOs) { + if (organizationApplicationDTO.getOrganization().getName().equals("Spring Test Organization")) { + assertThat(organizationApplicationDTO.getOrganization().getUserPermissions().contains("read:organizations")); + + Application application = organizationApplicationDTO.getApplications().get(0); + assertThat(application.getUserPermissions()).contains("read:applications"); + assertThat(application.isAppIsExample()).isFalse(); + } + } - OrganizationApplicationsDTO orgAppDto = organizationApplications.get(0); - assertThat(orgAppDto.getOrganization().getUserPermissions().contains("read:organizations")); - Application application = orgAppDto.getApplications().get(0); - assertThat(application.getUserPermissions()).contains("read:applications"); - assertThat(application.isAppIsExample()).isFalse(); }) .verifyComplete(); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java index 24891b514bc..2d664ccffe3 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java @@ -726,7 +726,7 @@ public void changeUserRoleAndCheckApplicationPermissionChanges() { UserRole userRole = new UserRole(); userRole.setUsername("usertest@usertest.com"); userRole.setRoleName("App Viewer"); - return userOrganizationService.updateRoleForMember(org.getId(), userRole); + return userOrganizationService.updateRoleForMember(org.getId(), userRole, "http://localhost:8080"); }); Mono applicationAfterRoleChange = organizationMono @@ -798,7 +798,7 @@ public void deleteUserRoleFromOrganizationTest() { userRole.setUsername("usertest@usertest.com"); // Setting the role name to null ensures that user is deleted from the organization userRole.setRoleName(null); - return userOrganizationService.updateRoleForMember(org.getId(), userRole); + return userOrganizationService.updateRoleForMember(org.getId(), userRole, "http://localhost:8080"); }); Mono> tupleMono = organizationMono