From 1bf1e561c5a46b9b168ced6c38a50e6fb50e8693 Mon Sep 17 00:00:00 2001 From: Akshay Sasidharan Date: Wed, 18 Oct 2023 15:10:24 +0530 Subject: [PATCH 01/25] remove unused postgrest proxy provider in import export module --- .../import_export_resources/import_export_resources.module.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/modules/import_export_resources/import_export_resources.module.ts b/server/src/modules/import_export_resources/import_export_resources.module.ts index f366f12aeb..5520a069bf 100644 --- a/server/src/modules/import_export_resources/import_export_resources.module.ts +++ b/server/src/modules/import_export_resources/import_export_resources.module.ts @@ -20,7 +20,6 @@ import { AppsService } from '@services/apps.service'; import { App } from 'src/entities/app.entity'; import { AppVersion } from 'src/entities/app_version.entity'; import { AppUser } from 'src/entities/app_user.entity'; -import { PostgrestProxyService } from '@services/postgrest_proxy.service'; const imports = [ PluginsModule, @@ -46,7 +45,6 @@ if (process.env.ENABLE_TOOLJET_DB === 'true') { PluginsHelper, AppsService, CredentialsService, - PostgrestProxyService, ], exports: [ImportExportResourcesService], }) From 339c7e54bce85467501f6c5291d425f05f6f3608 Mon Sep 17 00:00:00 2001 From: Mekhla Asopa <59684099+Mekhla-Asopa@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:14:57 +0530 Subject: [PATCH 02/25] Updated cypess mysql spec (#7717) --- .../e2e/editor/data-source/mysqlHappyPath.cy.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cypress-tests/cypress/e2e/editor/data-source/mysqlHappyPath.cy.js b/cypress-tests/cypress/e2e/editor/data-source/mysqlHappyPath.cy.js index f8ad149d8b..230baf3a29 100644 --- a/cypress-tests/cypress/e2e/editor/data-source/mysqlHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/editor/data-source/mysqlHappyPath.cy.js @@ -138,7 +138,7 @@ describe("Data sources MySql", () => { fillDataSourceTextField( postgreSqlText.labelDbName, postgreSqlText.placeholderNameOfDB, - "testdb" + "testdv" ); fillDataSourceTextField( postgreSqlText.labelUserName, @@ -205,7 +205,7 @@ describe("Data sources MySql", () => { fillConnectionForm({ Host: Cypress.env("mysql_host"), Port: Cypress.env("mysql_port"), - "Database Name": "testdb", + "Database Name": "testdv", Username: Cypress.env("mysql_user"), Password: Cypress.env("mysql_password"), }); @@ -383,7 +383,7 @@ describe("Data sources MySql", () => { fillConnectionForm({ Host: Cypress.env("mysql_host"), Port: Cypress.env("mysql_port"), - "Database Name": "testdb", + "Database Name": "testdv", Username: Cypress.env("mysql_user"), Password: Cypress.env("mysql_password"), }); @@ -416,7 +416,7 @@ describe("Data sources MySql", () => { cy.get(".p-3").should( "have.text", - `[{"Tables_in_testdb (${dbName})":"${dbName}"}]` + `[{"Tables_in_testdv (${dbName})":"${dbName}"}]` ); // addQuery( @@ -458,7 +458,7 @@ describe("Data sources MySql", () => { fillConnectionForm({ Host: Cypress.env("mysql_host"), Port: "3318", - "Database Name": "testdb", + "Database Name": "testdv", Username: Cypress.env("mysql_user"), Password: Cypress.env("mysql_password"), }); From 2d97089e79aacc94ff3ee35e6d6effc6bde79ba4 Mon Sep 17 00:00:00 2001 From: "Vishnu.Nemarugommula" <35486999+nemarugommula@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:36:07 +0530 Subject: [PATCH 03/25] Create a new Bucket functionality for S3 Issue:#5690 --- marketplace/plugins/s3/lib/index.ts | 4 ++++ marketplace/plugins/s3/lib/operations.json | 17 +++++++++++++++++ marketplace/plugins/s3/lib/query_operations.ts | 10 +++++++++- marketplace/plugins/s3/lib/types.ts | 1 + plugins/packages/s3/lib/index.ts | 4 ++++ plugins/packages/s3/lib/operations.json | 17 +++++++++++++++++ plugins/packages/s3/lib/operations.ts | 8 +++++++- plugins/packages/s3/lib/types.ts | 1 + 8 files changed, 60 insertions(+), 2 deletions(-) diff --git a/marketplace/plugins/s3/lib/index.ts b/marketplace/plugins/s3/lib/index.ts index eb3c2b5513..815129789d 100644 --- a/marketplace/plugins/s3/lib/index.ts +++ b/marketplace/plugins/s3/lib/index.ts @@ -1,4 +1,5 @@ import { + createBucket, getObject, uploadObject, listBuckets, @@ -19,6 +20,9 @@ export default class S3QueryService implements QueryService { try { switch (operation) { + case Operation.CreateBucket: + result = await createBucket(client, queryOptions); + break; case Operation.ListBuckets: result = await listBuckets(client, {}); break; diff --git a/marketplace/plugins/s3/lib/operations.json b/marketplace/plugins/s3/lib/operations.json index 7b7dcff117..2dd5cc2db1 100644 --- a/marketplace/plugins/s3/lib/operations.json +++ b/marketplace/plugins/s3/lib/operations.json @@ -13,6 +13,10 @@ "type": "dropdown-component-flip", "description": "Single select dropdown for operation", "list": [ + { + "value": "create_bucket", + "name": "Create a new bucket" + }, { "value": "get_object", "name": "Read object" @@ -43,6 +47,19 @@ } ] }, + "create_bucket": { + "bucket": { + "label": "Bucket Name", + "key": "bucket", + "type": "codehinter", + "lineNumbers": false, + "description": "Enters a name for the new bucket", + "width": "320px", + "height": "36px", + "className": "codehinter-plugins", + "placeholder": "Enter New Bucket Name" + } + }, "get_object": { "bucket": { "label": "Bucket", diff --git a/marketplace/plugins/s3/lib/query_operations.ts b/marketplace/plugins/s3/lib/query_operations.ts index 9f61a10a29..4493f56e98 100644 --- a/marketplace/plugins/s3/lib/query_operations.ts +++ b/marketplace/plugins/s3/lib/query_operations.ts @@ -3,8 +3,9 @@ import { ListBucketsCommand, PutObjectCommand, DeleteObjectCommand, - S3Client, ListObjectsV2Command, + CreateBucketCommand, + S3Client, } from '@aws-sdk/client-s3'; // https://aws.amazon.com/blogs/developer/generate-presigned-url-modular-aws-sdk-javascript/ import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; @@ -38,6 +39,13 @@ export async function signedUrlForGet(client: S3Client, options: QueryOptions): return { url }; } +export async function createBucket(client: S3Client, options: QueryOptions): Promise { + const createBucketCommand = new CreateBucketCommand({ + Bucket: options.bucket, + }); + return await client.send(createBucketCommand); +} + export async function getObject(client: S3Client, options: QueryOptions): Promise { // Create a helper function to convert a ReadableStream to a string. const streamToString = (stream) => diff --git a/marketplace/plugins/s3/lib/types.ts b/marketplace/plugins/s3/lib/types.ts index f83f557a96..650da1efbf 100644 --- a/marketplace/plugins/s3/lib/types.ts +++ b/marketplace/plugins/s3/lib/types.ts @@ -15,6 +15,7 @@ export type QueryOptions = { }; export enum Operation { + CreateBucket = 'create_bucket', ListBuckets = 'list_buckets', ListObjects = 'list_objects', GetObject = 'get_object', diff --git a/plugins/packages/s3/lib/index.ts b/plugins/packages/s3/lib/index.ts index ddf825a9fb..2ebe19585a 100644 --- a/plugins/packages/s3/lib/index.ts +++ b/plugins/packages/s3/lib/index.ts @@ -1,4 +1,5 @@ import { + createBucket, getObject, uploadObject, listBuckets, @@ -23,6 +24,9 @@ export default class S3QueryService implements QueryService { try { switch (operation) { + case Operation.CreateBucket: + result = await createBucket(client, queryOptions); + break; case Operation.ListBuckets: result = await listBuckets(client, {}); break; diff --git a/plugins/packages/s3/lib/operations.json b/plugins/packages/s3/lib/operations.json index 69622bd6b7..026f68ce73 100644 --- a/plugins/packages/s3/lib/operations.json +++ b/plugins/packages/s3/lib/operations.json @@ -13,6 +13,10 @@ "type": "dropdown-component-flip", "description": "Single select dropdown for operation", "list": [ + { + "value": "create_bucket", + "name": "Create a new bucket" + }, { "value": "get_object", "name": "Read object" @@ -43,6 +47,19 @@ } ] }, + "create_bucket": { + "bucket": { + "label": "Bucket Name", + "key": "bucket", + "type": "codehinter", + "lineNumbers": false, + "description": "Enters a name for the new bucket", + "width": "320px", + "height": "36px", + "className": "codehinter-plugins", + "placeholder": "Enter New Bucket Name" + } + }, "get_object": { "bucket": { "label": "Bucket", diff --git a/plugins/packages/s3/lib/operations.ts b/plugins/packages/s3/lib/operations.ts index 9f61a10a29..fc10da84c8 100644 --- a/plugins/packages/s3/lib/operations.ts +++ b/plugins/packages/s3/lib/operations.ts @@ -4,6 +4,7 @@ import { PutObjectCommand, DeleteObjectCommand, S3Client, + CreateBucketCommand, ListObjectsV2Command, } from '@aws-sdk/client-s3'; // https://aws.amazon.com/blogs/developer/generate-presigned-url-modular-aws-sdk-javascript/ @@ -37,7 +38,12 @@ export async function signedUrlForGet(client: S3Client, options: QueryOptions): }); return { url }; } - +export async function createBucket(client: S3Client, options: QueryOptions): Promise { + const createBucketCommand = new CreateBucketCommand({ + Bucket: options.bucket, + }); + return await client.send(createBucketCommand); +} export async function getObject(client: S3Client, options: QueryOptions): Promise { // Create a helper function to convert a ReadableStream to a string. const streamToString = (stream) => diff --git a/plugins/packages/s3/lib/types.ts b/plugins/packages/s3/lib/types.ts index c43ab0bd0a..4cb9549493 100644 --- a/plugins/packages/s3/lib/types.ts +++ b/plugins/packages/s3/lib/types.ts @@ -19,6 +19,7 @@ export type QueryOptions = { }; export enum Operation { + CreateBucket = 'create_bucket', ListBuckets = 'list_buckets', ListObjects = 'list_objects', GetObject = 'get_object', From a03b73d4a80aa9c8f9cb7a6e919e4aa8596f6740 Mon Sep 17 00:00:00 2001 From: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:12:57 +0530 Subject: [PATCH 04/25] Bugfix/App export should also export tables in join query - Design Review Changes (#7806) * basic and static join query executed * tooljetDB Join operation flow - work inprogress * complete flow for tooljetdb join pending testing and minor changes * updated constructHavingStatement method logic to support aggregation function and added comments * worki in progress tooljetDB Join * feat: added basic layout for tjdb join fields * feat: dropdown support for icons * feat: working on where condition ui in join * feat: added base layout for filter and sort in tooljetdb join * feat: added multi select support and minor style changes * feat: support default value for selectbox * feat: dd select styling added * style: override vanilla dd select styles with tj styles * fix: fixed minor UI issues in select box * feat: added select section layout * feat: added hooks state for join options * feat: load all added tables columns * feat: working on where section logic * feat: join constraints UI * feat: filter condition dropdowns added * feat: join widget for join query op in tjdb * feat: sort section base UI * feat: select widget for join query in tjdb * feat: filter section add option and delete option done * feat: update filter condition logic added * feat: added onchange event for operator and rhs values update * feat: added sort dropdown for tjdb join * feat: base logic for Filters in join query * fix: removed comments and added validation for fetching table details * feat: add limit option logic * feat: backend api has been integrated for tooljetdb joins * added icons to solid icons * fix: jsconfig auto save lint fix * fix: update from table when selected table changes * feat: added from to join table options in tjdb dq * fix: added fetching tables list for JSON in backend * fix: fixed json data for join query * fix: temp fix for fields with empty object * feat: added icon support for dd select * fix: added default state to avoid error in conditionlist * fix: limit tables selection to already joined tables in tjdb join * fix: empty values to orderBy, filters and limit will remove the option from json * fix: in json first level empty value scenario has been handled * fix: select in tooljetdb join query can have multiple columns with same name handled by adding prefix tablename_ to the column name * fix: restrict selectable tables in join contraints * feat: reset join constraints when invlaid joins added * fix: empty values will not be allowed UI validation * fix: codehinter border has been removed * fix: recalculate join data when join tables change * fix: corrected options length calc for showing search box * fix: filter table dropdown must contain only selected tables from join section * fix: empty values validation has been removed * fix: add from attribute to join options * fix: alias is added to all the table column * feat: selected option in Select section will be at the top * fix: reset joins when selected table changed * fix: drop down focus ui * feat: autoselect all columns by defualt for join select * feat: restrict column selection to same datatype * fix: removed blank table names from select * feat: added tooltip for info * fix: removed duplicate tooltip * fix: add button in table dropdown * fix: added from table object back * feat: tjdb join select dropdown select all cols by default * fix: add new table button name corrected * feat: no table selected error message * feat: add select style for select dropdown * style: updated dropdown select style to match new theme * feat: added alert modal for deleting joins * feat: hardcode operator since once one option available at the moment * style: fix icon styles for dropdown * feat: created reusable confirm dialogue * fix: fixed bug for nested dropdowns * fix; cache select components to prevent unnecessory rerenders * feat: reused the common popup on updating the tables * fix: info popup will trigger only if table is already exists * fix: fixed bug that caused edit to break for tjdb join * style: fixed spacing for tjdb join components * fix: select section all options cant be deselected issue fixed * fix: add info icon for empty filter and sort component * feat: offset fature for joins has been added * fix: layout fixed to incorporate filter dropdown with text * fix: basic validation in UI for mandatory and non-mandatory fields * feat: more options added for filter in joins * fix: added filter option for regular expression * fix: fixed wrong autoupdate of join fields * style: updated badge color w.r.t theme * fix: removed the commented code * style fixes * refactor: changed tooljetdb join logic based on tableId instead of name * fix: joins table value is not been shown after save * fix: CSS design fix and removed not required commented codes * feat: tableid to table name mapping in error * fix: errors will be shown in the debugger for tooljetdb join * stylefix: container for join sort and select made full width * stylefix: changed CTA test in popup spacing issue adjusted * fix: few PR review comments to refactor has been done * fix:random id generator has been removed and uuid has been used * feat: Select all functionality in Select Drop down has been added * fix: first time AND operator has been removed * fix:Sort Section - Removed table were listed in the drop down * fix: add more in join section deleting newly created joins * fix: select section total selected count was wrong * stylefix: dropdown menu height has been reduced * fix: sort section on join query will have prefix table name along with column name * feat: changed the select drop down with add new table option * fix: center align text only for join operator drop down * fix join icons to be centred * reduce chevron icon size * fix:error handling by status code * feat: added placeholder for empty select box * fix: fixed the PR comments * stylefix:multi select with checkbox will not have tick and bgcolor * stylefix: codehinter doesnt expand entire row content * stylefix: codehinter placeholder is center aligned scroll has been removed and overflow content has been handled * stylefix: codehinter font size made to 12 * feat: offset option for list rows in tjdb query * inprogress: tjdb joins tables must be exported * Updated cypess mysql spec (#7717) * fix: import app missed the tjdb tables in join query --------- Co-authored-by: Johnson Cherian Co-authored-by: Akshay Sasidharan Co-authored-by: Mekhla Asopa <59684099+Mekhla-Asopa@users.noreply.github.com> --- .../TooljetDatabase/DropDownSelect.jsx | 7 +- .../TooljetDatabase/JoinConstraint.jsx | 77 +++++++++++++---- .../TooljetDatabase/JoinSelect.jsx | 18 +++- .../QueryEditors/TooljetDatabase/JoinSort.jsx | 15 ++-- .../TooljetDatabase/JoinTable.jsx | 78 +++++++++++------ .../QueryEditors/TooljetDatabase/ListRows.jsx | 21 ++++- .../TooljetDatabase/SelectBox.jsx | 14 ++-- .../TooljetDatabase/ToolJetDbOperations.jsx | 14 +++- .../TooljetDatabase/operations.js | 3 +- frontend/src/_styles/queryManager.scss | 6 ++ .../src/services/app_import_export.service.ts | 84 ++++++++++++++++++- server/src/services/apps.service.ts | 23 ++++- server/src/services/tooljet_db.service.ts | 4 +- 13 files changed, 296 insertions(+), 68 deletions(-) diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx index 41d39663f0..90d708ab71 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx @@ -22,6 +22,8 @@ const DropDownSelect = ({ emptyError, shouldCenterAlignText = false, showPlaceHolder = false, + highlightSelected = true, + buttonClasses = '', }) => { const popoverId = useRef(`dd-select-${uuidv4()}`); const popoverBtnId = useRef(`dd-select-btn-${uuidv4()}`); @@ -124,11 +126,12 @@ const DropDownSelect = ({ onAdd={onAdd} addBtnLabel={addBtnLabel} emptyError={emptyError} + highlightSelected={highlightSelected} /> } > - +
-
+ ); }; diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx index 0b742dc9bd..0769e43eb6 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinConstraint.jsx @@ -88,13 +88,22 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => { )} - - -
Join
+ + +
+ Join +
- + {index ? ( { value={leftTableList.find((val) => val?.value === leftFieldTable)} /> ) : ( -
{baseTableDetails?.table_name ?? ''}
+
+ {baseTableDetails?.table_name ?? ''} +
)} - + { { }} /> ))} - + + 1 @@ -295,6 +314,7 @@ const JoinOn = ({ > {index == 1 && ( )} - {index == 0 &&
On
} + {index == 0 && ( +
+ On +
+ )} {index > 1 && ( -
+
{groupOperator}
)} - + - + {/* {operator}
+
+ {operator} +
{index > 0 && ( - + )} diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx index 4239e7ef0a..dd8413a475 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinSelect.jsx @@ -90,12 +90,22 @@ export default function JoinSelect({ darkMode }) { const respectiveTableSelectedOptions = joinSelectOptions.filter((val) => val?.table === table); const respectiveTableOptions = tableOptions[table] ?? []; return ( - - -
{findTableDetails(table)?.table_name ?? ''}
+ + +
+ {findTableDetails(table)?.table_name ?? ''} +
- + { const tableDetails = options?.table ? findTableDetails(options?.table) : ''; return ( - - + + -
+
setJoinOrderByOptions(joinOrderByOptions.filter((opt, idx) => idx !== i))} > @@ -137,7 +142,7 @@ export default function JoinSort({ darkMode }) { }) )} {/* Dynamically render below Row */} - + setJoinOrderByOptions([...joinOrderByOptions, {}])}> diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx index 0e80238287..25cb00b5df 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx @@ -90,8 +90,8 @@ const SelectTableMenu = ({ darkMode }) => {
{/* Join Section */}
- -
+ +
{joins.map((join, joinIndex) => ( {
{/* Filter Section */}
- -
+ +
{/* Sort Section */}
- -
+ +
{/* Limit Section */}
- -
+ +
{
{/* Offset Section */}
- -
+ +
{
{/* Select Section */}
- -
+ +
@@ -364,10 +364,11 @@ const RenderFilterSection = ({ darkMode }) => { const { operator = '', leftField = {}, rightField = {} } = conditionDetail; const LeftSideTableDetails = leftField?.table ? findTableDetails(leftField?.table) : ''; return ( - - + + {index === 1 && ( updateOperatorForConditions(change?.value)} options={groupOperators} @@ -375,11 +376,32 @@ const RenderFilterSection = ({ darkMode }) => { value={groupOperators.find((op) => op.value === conditions.operator)} /> )} - {index === 0 &&
Where
} - {index > 1 &&
{conditions?.operator}
} + {index === 0 && ( +
+ Where +
+ )} + {index > 1 && ( +
+ {conditions?.operator} +
+ )} - + updateFilterConditionEntry('Column', index, { @@ -399,8 +421,9 @@ const RenderFilterSection = ({ darkMode }) => { darkMode={darkMode} /> - + updateFilterConditionEntry('Operator', index, { operator: change?.value })} value={filterOperatorOptions.find((op) => op.value === operator)} @@ -409,9 +432,10 @@ const RenderFilterSection = ({ darkMode }) => { /> -
+
{operator === 'IS' ? ( updateFilterConditionEntry('Value', index, { value: change?.value, isLeftSideCondition: false }) @@ -429,9 +453,9 @@ const RenderFilterSection = ({ darkMode }) => { : JSON.stringify(rightField?.value) : rightField?.value } - className="codehinter-plugins" + className="border border-end-0 fs-12 tjdb-codehinter" theme={darkMode ? 'monokai' : 'default'} - height={'28px'} + height={'30px'} placeholder="Value" onChange={(newValue) => updateFilterConditionEntry('Value', index, { value: newValue, isLeftSideCondition: false }) @@ -439,10 +463,14 @@ const RenderFilterSection = ({ darkMode }) => { /> )}
+ removeFilterConditionEntry(index)} > @@ -470,7 +498,7 @@ const RenderFilterSection = ({ darkMode }) => { )} {filterComponents} - + addNewFilterConditionEntry()}> diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/ListRows.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/ListRows.jsx index 4944a64ae8..b86c00f8f0 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/ListRows.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/ListRows.jsx @@ -9,7 +9,8 @@ import { isOperatorOptions } from './util'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; export const ListRows = React.memo(({ darkMode }) => { - const { columns, listRowsOptions, limitOptionChanged, handleOptionsChange } = useContext(TooljetDatabaseContext); + const { columns, listRowsOptions, limitOptionChanged, handleOptionsChange, offsetOptionChanged } = + useContext(TooljetDatabaseContext); function handleWhereFiltersChange(filters) { handleOptionsChange('where_filters', filters); @@ -155,7 +156,7 @@ export const ListRows = React.memo(({ darkMode }) => {
{/* Limit */} -
+
@@ -170,6 +171,22 @@ export const ListRows = React.memo(({ darkMode }) => { />
+ {/* Offset */} +
+ +
+ offsetOptionChanged(newValue)} + /> +
+
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/SelectBox.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/SelectBox.jsx index 96d7d5c090..61e538f91c 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/SelectBox.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/SelectBox.jsx @@ -19,6 +19,7 @@ function DataSourceSelect({ addBtnLabel, selected, emptyError, + highlightSelected, }) { const handleChangeDataSource = (source) => { onSelect && onSelect(source); @@ -88,7 +89,7 @@ function DataSourceSelect({ /> ))} {children} - {props.isSelected && ( + {props.isSelected && highlightSelected && ( ({ ...prev, limit: value })); }; + const offsetOptionChanged = (value) => { + setListRowsOptions((prev) => ({ ...prev, offset: value })); + }; + const deleteOperationLimitOptionChanged = (limit) => { setDeleteRowsOptions((prev) => ({ ...prev, limit: limit })); }; @@ -290,6 +294,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay listRowsOptions, setListRowsOptions, limitOptionChanged, + offsetOptionChanged, handleOptionsChange, deleteRowsOptions, handleDeleteRowsOptionsChange, @@ -467,9 +472,10 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay {/* table name dropdown */}
- -
+ +
- -
+ +
0) { + const joinsTableIdUpdatedList = joinOptions.joins.map((joinCondition) => { + const updatedJoinCondition = { ...joinCondition }; + // Updating Join tableId + if (updatedJoinCondition.table) + updatedJoinCondition.table = + tooljetDatabaseMapping[updatedJoinCondition.table]?.id ?? updatedJoinCondition.table; + // Updating TableId on Conditions in Join Query + if (updatedJoinCondition.conditions) { + const updatedJoinConditionFilter = this.updateNewTableIdForFilter( + updatedJoinCondition.conditions, + tooljetDatabaseMapping + ); + updatedJoinCondition.conditions = updatedJoinConditionFilter.conditions; + } + + return updatedJoinCondition; + }); + joinOptions.joins = joinsTableIdUpdatedList; + } + + // Filter Section + if (joinOptions?.conditions) { + joinOptions.conditions = this.updateNewTableIdForFilter( + joinOptions.conditions, + tooljetDatabaseMapping + ).conditions; + } + + // Select Section + if (joinOptions?.fields) { + joinOptions.fields = joinOptions.fields.map((eachField) => { + if (eachField.table) { + eachField.table = tooljetDatabaseMapping[eachField.table]?.id ?? eachField.table; + return eachField; + } + return eachField; + }); + } + + // From Section + if (joinOptions?.from) { + const { name = '' } = joinOptions.from; + joinOptions.from = { ...joinOptions.from, name: tooljetDatabaseMapping[name]?.id ?? name }; + } + + // Sort Section + if (joinOptions?.order_by) { + joinOptions.order_by = joinOptions.order_by.map((eachOrderBy) => { + if (eachOrderBy.table) { + eachOrderBy.table = tooljetDatabaseMapping[eachOrderBy.table]?.id ?? eachOrderBy.table; + return eachOrderBy; + } + return eachOrderBy; + }); + } + + return { ...queryOptions, table_id: tooljetDatabaseMapping[queryOptions.table_id]?.id, join_table: joinOptions }; + } else { + return { ...queryOptions, table_id: tooljetDatabaseMapping[queryOptions.table_id]?.id }; + } + } + + updateNewTableIdForFilter(joinConditions, tooljetDatabaseMapping) { + const { conditionsList = [] } = { ...joinConditions }; + const updatedConditionList = conditionsList.map((condition) => { + if (condition.conditions) { + return this.updateNewTableIdForFilter(condition.conditions, tooljetDatabaseMapping); + } else { + const { operator = '=', leftField = {}, rightField = {} } = { ...condition }; + if (leftField?.type && leftField.type === 'Column') + leftField['table'] = tooljetDatabaseMapping[leftField.table]?.id ?? leftField.table; + if (rightField?.type && rightField.type === 'Column') + rightField['table'] = tooljetDatabaseMapping[rightField.table]?.id ?? rightField.table; + return { operator, leftField, rightField }; + } + }); + return { conditions: { ...joinConditions, conditionsList: [...updatedConditionList] } }; } async updateEventActionsForNewVersionWithNewMappingIds( diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index 4f3727e82b..b5657f9a38 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -977,9 +977,28 @@ export class AppsService { .andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' }) .getMany(); - const uniqTableIds = [...new Set(tooljetDbDataQueries.map((dq) => dq.options['table_id']))]; + const uniqTableIds = new Set(); + tooljetDbDataQueries.forEach((dq) => { + if (dq.options?.operation === 'join_tables') { + const joinOptions = dq.options?.join_table?.joins ?? []; + (joinOptions || []).forEach((join) => { + const { table, conditions } = join; + if (table) uniqTableIds.add(table); + conditions?.conditionsList?.forEach((condition) => { + const { leftField, rightField } = condition; + if (leftField?.table) { + uniqTableIds.add(leftField?.table); + } + if (rightField?.table) { + uniqTableIds.add(rightField?.table); + } + }); + }); + } + if (dq.options.table_id) uniqTableIds.add(dq.options.table_id); + }); - return uniqTableIds.map((table_id) => { + return [...uniqTableIds].map((table_id) => { return { table_id }; }); }); diff --git a/server/src/services/tooljet_db.service.ts b/server/src/services/tooljet_db.service.ts index 0b422e71a4..49dedea3b0 100644 --- a/server/src/services/tooljet_db.service.ts +++ b/server/src/services/tooljet_db.service.ts @@ -3,6 +3,7 @@ import { EntityManager, In, QueryFailedError } from 'typeorm'; import { InjectEntityManager } from '@nestjs/typeorm'; import { InternalTable } from 'src/entities/internal_table.entity'; import { isString, isEmpty } from 'lodash'; +import { PostgrestProxyService } from '@services/postgrest_proxy.service'; export type TableColumnSchema = { column_name: string; @@ -23,7 +24,8 @@ export class TooljetDbService { private readonly manager: EntityManager, @Optional() @InjectEntityManager('tooljetDb') - private readonly tooljetDbManager: EntityManager + private readonly tooljetDbManager: EntityManager, + private readonly postgrestProxyService: PostgrestProxyService ) {} async perform(organizationId: string, action: string, params = {}) { From a13ad5a6a9700c0fdea2b45ef85e480c2299cadf Mon Sep 17 00:00:00 2001 From: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:18:03 +0530 Subject: [PATCH 05/25] Slack plugin oauth flow not working (#7702) * fix: slack oauth access token undefined error fixed - but query returns un-authorized * fix: slack oauth issue fixed * fix: removed the commented code --- frontend/src/_components/Slack.jsx | 29 ++++++------ server/src/services/data_queries.service.ts | 52 +++++++++++++++------ server/src/services/data_sources.service.ts | 4 +- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/frontend/src/_components/Slack.jsx b/frontend/src/_components/Slack.jsx index 5c90932a92..46df2015d1 100644 --- a/frontend/src/_components/Slack.jsx +++ b/frontend/src/_components/Slack.jsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { datasourceService } from '@/_services'; import { useTranslation } from 'react-i18next'; - +import { toast } from 'react-hot-toast'; import Button from '@/_ui/Button'; -const Slack = ({ optionchanged, createDataSource, options, isSaving, selectedDataSource }) => { +const Slack = ({ optionchanged, createDataSource, options, isSaving, _selectedDataSource }) => { const [authStatus, setAuthStatus] = useState(null); const { t } = useTranslation(); @@ -18,19 +18,22 @@ const Slack = ({ optionchanged, createDataSource, options, isSaving, selectedDat scope = `${scope},chat:write`; } - datasourceService.fetchOauth2BaseUrl(provider).then((data) => { - const authUrl = `${data.url}&scope=${scope}&access_type=offline&prompt=select_account`; - if (selectedDataSource?.id) { - localStorage.setItem('sourceWaitingForOAuth', selectedDataSource.id); - } else { + datasourceService + .fetchOauth2BaseUrl(provider) + .then((data) => { + const authUrl = `${data.url}&scope=${scope}&access_type=offline&prompt=select_account`; + localStorage.setItem('sourceWaitingForOAuth', 'newSource'); - } - optionchanged('provider', provider).then(() => { - optionchanged('oauth2', true); + optionchanged('provider', provider).then(() => { + optionchanged('oauth2', true); + }); + setAuthStatus('waiting_for_token'); + window.open(authUrl); + }) + .catch(({ error }) => { + toast.error(error); + setAuthStatus(null); }); - setAuthStatus('waiting_for_token'); - window.open(authUrl); - }); } function saveDataSource() { diff --git a/server/src/services/data_queries.service.ts b/server/src/services/data_queries.service.ts index 16cc796238..458e6ba65b 100644 --- a/server/src/services/data_queries.service.ts +++ b/server/src/services/data_queries.service.ts @@ -14,6 +14,7 @@ import { EncryptionService } from './encryption.service'; import { App } from 'src/entities/app.entity'; import { AppEnvironmentService } from './app_environments.service'; import { dbTransactionWrap } from 'src/helpers/utils.helper'; +import allPlugins from '@tooljet/plugins/dist/server'; import { DataSourceScopes } from 'src/helpers/data_source.constants'; import { EventHandler } from 'src/entities/event_handler.entity'; @@ -351,6 +352,22 @@ export class DataQueriesService { } }; + /* this function only for getting auth token for googlesheets and related plugins*/ + async fetchAPITokenFromPlugins(dataSource: DataSource, code: string, sourceOptions: any) { + const queryService = new allPlugins[dataSource.kind](); + const accessDetails = await queryService.accessDetailsFrom(code, sourceOptions); + const options = []; + for (const row of accessDetails) { + const option = {}; + option['key'] = row[0]; + option['value'] = row[1]; + option['encrypted'] = true; + + options.push(option); + } + return options; + } + /* This function fetches access token from authorization code */ async authorizeOauth2( dataSource: DataSource, @@ -360,22 +377,27 @@ export class DataQueriesService { organizationId?: string ): Promise { const sourceOptions = await this.parseSourceOptions(dataSource.options, organizationId, environmentId); - const isMultiAuthEnabled = dataSource.options['multiple_auth_enabled']?.value; - const newToken = await this.fetchOAuthToken(sourceOptions, code, userId, isMultiAuthEnabled); - const tokenData = this.getCurrentToken( - isMultiAuthEnabled, - dataSource.options['tokenData']?.value, - newToken, - userId - ); + let tokenOptions: any; + if (['googlesheets', 'slack', 'zendesk'].includes(dataSource.kind)) { + tokenOptions = await this.fetchAPITokenFromPlugins(dataSource, code, sourceOptions); + } else { + const isMultiAuthEnabled = dataSource.options['multiple_auth_enabled']?.value; + const newToken = await this.fetchOAuthToken(sourceOptions, code, userId, isMultiAuthEnabled); + const tokenData = this.getCurrentToken( + isMultiAuthEnabled, + dataSource.options['tokenData']?.value, + newToken, + userId + ); - const tokenOptions = [ - { - key: 'tokenData', - value: tokenData, - encrypted: false, - }, - ]; + tokenOptions = [ + { + key: 'tokenData', + value: tokenData, + encrypted: false, + }, + ]; + } await this.dataSourcesService.updateOptions(dataSource.id, tokenOptions, organizationId, environmentId); return; diff --git a/server/src/services/data_sources.service.ts b/server/src/services/data_sources.service.ts index 875ecc9983..b4e1ff72ce 100644 --- a/server/src/services/data_sources.service.ts +++ b/server/src/services/data_sources.service.ts @@ -411,7 +411,9 @@ export class DataSourcesService { for (const option of optionsWithOauth) { if (option['encrypted']) { const existingCredentialId = - dataSource.options[option['key']] && dataSource.options[option['key']]['credential_id']; + dataSource?.options && + dataSource.options[option['key']] && + dataSource.options[option['key']]['credential_id']; if (existingCredentialId) { (option['value'] || option['value'] === '') && From 05b861ab29265819237fea6c93591ef860252d5d Mon Sep 17 00:00:00 2001 From: Syed Abdul Rahman <137684137+S-Abdul-Rahman@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:19:37 +0530 Subject: [PATCH 06/25] Fixed Filter button active state issue (#7663) * Fixed Filter button active state issue * Fixed icon size changes in active state issue --- frontend/src/_styles/theme.scss | 122 ++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 992a467389..93477a3af4 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -12,6 +12,7 @@ @import "./ui-operations.scss"; @import 'react-loading-skeleton/dist/skeleton.css'; @import './table-component.scss'; + /* ibm-plex-sans-100 - latin */ @font-face { font-display: swap; @@ -269,7 +270,8 @@ button { .emoji-mart-scroll+.emoji-mart-bar { display: none; } -.accordion-item{ + +.accordion-item { border: solid var(--slate5); border-width: 0px 0px 1px 0px; } @@ -301,6 +303,7 @@ button { .accordion-body { padding: 6px 16px 20px 16px !important; + .form-label { font-weight: 400; font-size: 12px; @@ -327,7 +330,7 @@ button { .resizer-select, .resizer-active { - border: solid 1px $primary !important; + border: solid 1px $primary !important; .top-right, .top-left, @@ -827,7 +830,7 @@ button { .list-group.list-group-transparent.dark .all-apps-link, .list-group-item-action.dark.active { - background-color: $dark-background !important; + background-color: $dark-background !important; } } @@ -1559,7 +1562,7 @@ button { .select-search-dark input { width: 224px !important; height: 32px !important; - border-radius: $border-radius !important; + border-radius: $border-radius !important; } } @@ -1570,7 +1573,7 @@ button { .select-search__value input, .select-search-dark input { height: 32px !important; - border-radius: $border-radius !important; + border-radius: $border-radius !important; } } @@ -1631,7 +1634,7 @@ button { -webkit-appearance: none; -moz-appearance: none; appearance: none; - border-radius: $border-radius !important; + border-radius: $border-radius !important; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @@ -1971,6 +1974,7 @@ button { text-align: center; color: #888; } + // jet-table-footer is common class used in other components other than table .jet-table-footer { .table-footer { @@ -2904,12 +2908,14 @@ input:focus-visible { width: 210px !important; //adjusted with padding box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08) !important; color: var(--slate12); + .flexbox-fix:nth-child(3) { div:nth-child(1) { - input{ + input { width: 100% !important; } - label{ + + label { color: var(--slate12) !important; } } @@ -3085,6 +3091,7 @@ input:focus-visible { .DateRangePickerInput__withBorder { border: 1px solid #1f2936; } + .main .canvas-container .canvas-area { background: #2f3c4c; } @@ -3673,7 +3680,7 @@ input[type="text"] { .nav-tabs .nav-link.active { font-weight: 400 !important; - color: $primary !important; + color: $primary !important; } .empty { @@ -4199,7 +4206,7 @@ input[type="text"] { .tabs-inspector.dark { .nav-link.active { - border-bottom: 1px solid $primary !important; + border-bottom: 1px solid $primary !important; } } @@ -4448,7 +4455,7 @@ input[type="text"] { } input { - border-radius: $border-radius !important; + border-radius: $border-radius !important; padding-left: 1.75rem !important; } } @@ -4617,8 +4624,8 @@ input[type="text"] { } .modal-content.home-modal-component.dark { - background-color: $bg-dark-light !important; - color: $white !important; + background-color: $bg-dark-light !important; + color: $white !important; .modal-title { color: $white !important; @@ -4651,22 +4658,22 @@ input[type="text"] { } .form-control { - border-color: $border-grey-dark !important; + border-color: $border-grey-dark !important; color: inherit; } input { - background-color: $bg-dark-light !important; + background-color: $bg-dark-light !important; } .form-select { - background-color: $bg-dark !important; - color: $white !important; - border-color: $border-grey-dark !important; + background-color: $bg-dark !important; + color: $white !important; + border-color: $border-grey-dark !important; } .text-muted { - color: $white !important; + color: $white !important; } } @@ -4977,7 +4984,7 @@ div#driver-page-overlay { } .dark-theme-walkthrough#driver-popover-item { - background-color: $bg-dark-light !important; + background-color: $bg-dark-light !important; border-color: rgba(101, 109, 119, 0.16) !important; .driver-popover-title { @@ -4985,7 +4992,7 @@ div#driver-page-overlay { } .driver-popover-tip { - border-color: transparent transparent transparent $bg-dark-light !important; + border-color: transparent transparent transparent $bg-dark-light !important; } .driver-popover-description { @@ -5017,7 +5024,7 @@ div#driver-page-overlay { .driver-next-btn, .driver-prev-btn { - color: $primary !important; + color: $primary !important; } .driver-disabled { @@ -5141,7 +5148,7 @@ div#driver-page-overlay { } .fx-canvas { - background:var(--slate4); + background: var(--slate4); padding: 0px; display: flex; height: 32px; @@ -5153,7 +5160,7 @@ div#driver-page-overlay { align-items: center; div { - background:var(--slate4) !important; + background: var(--slate4) !important; display: flex; justify-content: center; align-items: center; @@ -5161,6 +5168,7 @@ div#driver-page-overlay { padding: 0px; } } + .org-name { color: var(--slate12) !important; font-size: 12px; @@ -5489,7 +5497,7 @@ div#driver-page-overlay { } .selected-node { - border-color: $primary-light !important; + border-color: $primary-light !important; } .json-tree-icon-container .selected-node>svg:first-child { @@ -5580,7 +5588,7 @@ div#driver-page-overlay { } .selected-node { - border-color: $primary-light !important; + border-color: $primary-light !important; } .selected-node .group-object-container .badge { @@ -5898,7 +5906,7 @@ div#driver-page-overlay { //Kanban board .kanban-container.dark-themed { - background-color: $bg-dark-light !important; + background-color: $bg-dark-light !important; .kanban-column { .card-header { @@ -5944,7 +5952,7 @@ div#driver-page-overlay { } .dnd-card.card.card-dark { - background-color: $bg-dark !important; + background-color: $bg-dark !important; } } @@ -7182,7 +7190,7 @@ tbody { } .application-brand { - a{ + a { height: 48px; position: relative; display: flex; @@ -7940,8 +7948,9 @@ tbody { width: 240px; height: 28px; flex-direction: row; - div{ - a{ + + div { + a { text-decoration: none; } } @@ -8786,7 +8795,7 @@ tbody { flex-direction: row !important; justify-content: center !important; align-items: center !important; - padding: 4px 16px !important; + //padding: 4px 16px !important; width: 100% !important; height: 28px !important; background: var(--grass2) !important; @@ -8799,7 +8808,7 @@ tbody { flex-direction: row !important; justify-content: center !important; align-items: center !important; - padding: 4px 16px !important; + //padding: 4px 16px !important; width: 100% !important; height: 28px !important; border-radius: 6px !important; @@ -10226,7 +10235,7 @@ tbody { border-radius: 6px !important; margin-bottom: 4px !important; color: var(--slate12) !important; - transition:none; + transition: none; &:hover { @@ -10250,13 +10259,15 @@ tbody { box-shadow: 0 0 0 1000px var(--base) inset !important; -webkit-text-fill-color: var(--slate12) !important; - &:hover { - box-shadow: 0 0 0 1000px var(--slate1) inset !important; - -webkit-text-fill-color: var(--slate12) !important;} + &:hover { + box-shadow: 0 0 0 1000px var(--slate1) inset !important; + -webkit-text-fill-color: var(--slate12) !important; + } - &:focus-visible { + &:focus-visible { box-shadow: 0 0 0 1000px var(--indigo2) inset !important; - -webkit-text-fill-color: var(--slate12) !important;} + -webkit-text-fill-color: var(--slate12) !important; + } } @@ -11822,14 +11833,17 @@ tbody { width: 170px !important; } } -.custom-gap-8{ + +.custom-gap-8 { gap: 8px; } -.color-slate-11{ + +.color-slate-11 { color: var(--slate11) !important; } -.custom-gap-6{ - gap:6px + +.custom-gap-6 { + gap: 6px } // ToolJet Database buttons @@ -11839,22 +11853,26 @@ tbody { padding: 4px 10px; } -.custom-gap-2{ - gap:2px +.custom-gap-2 { + gap: 2px } -.custom-gap-4{ + +.custom-gap-4 { gap: 4px; } -.text-black-000{ + +.text-black-000 { color: var(--text-black-000) !important; } -.custom-gap-12{ - gap:12px + +.custom-gap-12 { + gap: 12px } -#inspector-tabpane-properties{ + +#inspector-tabpane-properties { .accordion { - .accordion-item:last-child{ + .accordion-item:last-child { border-bottom: none !important; } } -} +} \ No newline at end of file From 5161053edefc3aa4efb2d780d966161da5a01dcb Mon Sep 17 00:00:00 2001 From: Syed Abdul Rahman <137684137+S-Abdul-Rahman@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:27:45 +0530 Subject: [PATCH 07/25] Fixed current version app export issue (#7831) * Fixed current version app export issue * removed isDownload variable * removed extra parameter in export function * added suggested changes --- frontend/src/HomePage/ExportAppModal.jsx | 460 ++++++++++++----------- 1 file changed, 243 insertions(+), 217 deletions(-) diff --git a/frontend/src/HomePage/ExportAppModal.jsx b/frontend/src/HomePage/ExportAppModal.jsx index d523f1d04b..7a3566c511 100644 --- a/frontend/src/HomePage/ExportAppModal.jsx +++ b/frontend/src/HomePage/ExportAppModal.jsx @@ -6,231 +6,257 @@ import { toast } from 'react-hot-toast'; import { ButtonSolid } from '@/_components/AppButton'; export default function ExportAppModal({ title, show, closeModal, customClassName, app, darkMode }) { - const currentVersion = app?.editing_version; - const [versions, setVersions] = useState(undefined); - const [tables, setTables] = useState(undefined); - const [versionId, setVersionId] = useState(currentVersion?.id); - const [exportTjDb, setExportTjDb] = useState(true); + const currentVersion = app?.editing_version; + const [versions, setVersions] = useState(undefined); + const [tables, setTables] = useState(undefined); + const [allTables, setAllTables] = useState(undefined); + const [versionId, setVersionId] = useState(currentVersion?.id); + const [exportTjDb, setExportTjDb] = useState(true); - useEffect(() => { - async function fetchAppVersions() { - try { - const fetchVersions = await appsService.getVersions(app.id); - const { versions } = fetchVersions; - setVersions(versions); - } catch (error) { - toast.error('Could not fetch the versions.', { - position: 'top-center', - }); - closeModal(); - } - } - async function fetchAppTables() { - try { - const fetchTables = await appsService.getTables(app.id); - const { tables } = fetchTables; - setTables(tables); - } catch (error) { - toast.error('Could not fetch the tables.', { - position: 'top-center', - }); - closeModal(); - } - } - fetchAppVersions(); - fetchAppTables(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const exportApp = (app, versionId, exportTjDb, tables) => { - const appOpts = { - app: [ - { - id: app.id, - ...(versionId && { search_params: { version_id: versionId } }), - }, - ], - }; - const requestBody = { - ...appOpts, - ...(exportTjDb && { tooljet_database: tables }), - organization_id: app.organization_id ?? app.organizationId, - }; - - appsService - .exportResource(requestBody) - .then((data) => { - const appName = app.name.replace(/\s+/g, '-').toLowerCase(); - const fileName = `${appName}-export-${new Date().getTime()}`; - // simulate link click download - const json = JSON.stringify(data, null, 2); - const blob = new Blob([json], { type: 'application/json' }); - const href = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = href; - link.download = fileName + '.json'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - closeModal(); - }) - .catch((error) => { - toast.error(`Could not export app: ${error.data.message}`, { - position: 'top-center', - }); - closeModal(); - }); - }; - - return ( - closeModal(false)} - contentClassName={`home-modal-component home-version-modal-component ${ - customClassName ? ` ${customClassName}` : '' - } ${darkMode && 'dark-theme'}`} - show={show} - backdrop={true} - keyboard={true} - enforceFocus={false} - animation={false} - onEscapeKeyDown={() => closeModal()} - centered - data-cy={'modal-component'} - > - - - {title} - - - - {Array.isArray(versions) ? ( - <> - -
-
- - Current Version - - -
- {versions.length >= 2 ? ( -
- - Other Versions - - {versions.map((version) => { - if (version.id !== currentVersion?.id) { - return ( - - ); + useEffect(() => { + async function fetchAppVersions() { + try { + const fetchVersions = await appsService.getVersions(app.id); + const { versions } = fetchVersions; + setVersions(versions); + } catch (error) { + toast.error('Could not fetch the versions.', { + position: 'top-center', + }); + closeModal(); + } + } + async function fetchAppTables() { + try { + const fetchTables = await appsService.getTables(app.id); // this is used to get all tables + const { tables } = fetchTables; + const tbl = await appsService.getAppByVersion(app.id, versionId) // this is used to get particular App by version + const { dataQueries } = tbl + const extractedIdData = []; + dataQueries.forEach(item => { + if (item.kind === "tooljetdb") { + const joinOptions = item.options?.join_table?.joins ?? []; + (joinOptions || []).forEach((join) => { + const { table, conditions } = join; + if (table) extractedIdData.push(table); + conditions?.conditionsList?.forEach((condition) => { + const { leftField, rightField } = condition; + if (leftField?.table) { + extractedIdData.push(leftField?.table); + } + if (rightField?.table) { + extractedIdData.push(rightField?.table); + } + }); + }); } - })} -
- ) : ( -
- No other versions found -
- )} -
-
-
- setExportTjDb(!exportTjDb)} /> -

Export ToolJet table schema

-
- - exportApp(app, null, exportTjDb, tables)} - > - Export All - - exportApp(app, versionId, exportTjDb, tables)} - > - Export selected version - - - - ) : ( - - )} -
- ); + }); + const uniqueSet = new Set(extractedIdData); + const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item })); + setTables(selectedVersiontable) + setAllTables(tables) + } catch (error) { + toast.error('Could not fetch the tables.', { + position: 'top-center', + }); + closeModal(); + } + } + fetchAppVersions(); + fetchAppTables(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [versionId]); + + const exportApp = (app, versionId, exportTjDb, exportTables) => { + const appOpts = { + app: [ + { + id: app.id, + ...(versionId && { search_params: { version_id: versionId } }), + }, + ], + }; + + const requestBody = { + ...appOpts, + ...(exportTjDb && { tooljet_database: exportTables }), + organization_id: app.organization_id, + }; + + appsService + .exportResource(requestBody) + .then((data) => { + const appName = app.name.replace(/\s+/g, '-').toLowerCase(); + const fileName = `${appName}-export-${new Date().getTime()}`; + // simulate link click download + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const href = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.download = fileName + '.json'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + closeModal(); + }) + .catch((error) => { + toast.error(`Could not export app: ${error.data.message}`, { + position: 'top-center', + }); + closeModal(); + }); + }; + + return ( + closeModal(false)} + contentClassName={`home-modal-component home-version-modal-component ${customClassName ? ` ${customClassName}` : '' + } ${darkMode && 'dark-theme'}`} + show={show} + backdrop={true} + keyboard={true} + enforceFocus={false} + animation={false} + onEscapeKeyDown={() => closeModal()} + centered + data-cy={'modal-component'} + > + + + {title} + + + + {Array.isArray(versions) ? ( + <> + +
+
+ + Current Version + + +
+ {versions.length >= 2 ? ( +
+ + Other Versions + + {versions.map((version) => { + if (version.id !== currentVersion?.id) { + return ( + + ); + } + })} +
+ ) : ( +
+ No other versions found +
+ )} +
+
+
+ setExportTjDb(!exportTjDb)} /> +

Export ToolJet table schema

+
+ + exportApp(app, null, exportTjDb, allTables)} + > + Export All + + exportApp(app, versionId, exportTjDb, tables)} + > + Export selected version + + + + ) : ( + + )} +
+ ); } function InputRadioField({ - versionId, - versionName, - versionCreatedAt, - checked = undefined, - key = undefined, - setVersionId, - className, + versionId, + versionName, + versionCreatedAt, + checked = undefined, + key = undefined, + setVersionId, + className, }) { - return ( - - setVersionId(target.value)} - style={{ marginLeft: '1rem' }} - className="cursor-pointer" - /> - - - ); + return ( + + setVersionId(target.value)} + style={{ marginLeft: '1rem' }} + className="cursor-pointer" + /> + + + ); } function Loader() { - return ( - -
-
Loading versions ...
-
-
-
- ); + return ( + +
+
Loading versions ...
+
+
+
+ ); } From ef4082a1f75bc37a6402347a5a85d239e37c174d Mon Sep 17 00:00:00 2001 From: Syed Abdul Rahman <137684137+S-Abdul-Rahman@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:50:23 +0530 Subject: [PATCH 08/25] Fixed query builder filter for ToolJet db (#7633) * Fixed query builder filter for ToolJet db * added condition for null value as well as renamed this hasEqualWithNull function name as hasEmptyStringOrNullValue * removed validation for empty string * Added validation for empty string * Adding release label to Trigger all the cypress worflow to main branch * marketplace workflow fix * [hot-fix] Pages `applications` handle issue (#8066) * resolved application page handle issue * fixed a typo * add: exporting the function * bumped the version * Empty strings and null values are now updating correctly --------- Co-authored-by: Adish M Co-authored-by: Adish M <44204658+adishM98@users.noreply.github.com> Co-authored-by: Muhsin Shah C P --- .../QueryEditors/TooljetDatabase/operations.js | 10 +++++----- .../QueryManager/QueryEditors/TooljetDatabase/util.js | 6 +++--- frontend/src/TooljetDatabase/Table/index.jsx | 9 +++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js index 407dcc39dc..96cd3af5b0 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js @@ -2,7 +2,7 @@ import { tooljetDatabaseService, authenticationService } from '@/_services'; import { isEmpty } from 'lodash'; import PostgrestQueryBuilder from '@/_helpers/postgrestQueryBuilder'; import { resolveReferences } from '@/_helpers/utils'; -import { hasEqualWithNull } from './util'; +import { hasEmptyStringOrNullValue } from './util'; export const tooljetDbOperations = { perform, @@ -46,7 +46,7 @@ function buildPostgrestQuery(filters) { postgrestQueryBuilder.order(column, order); } - if (!isEmpty(column) && !isEmpty(operator) && value && value !== '') { + if (!isEmpty(column) && !isEmpty(operator)) { postgrestQueryBuilder[operator](column, value.toString()); } } @@ -57,7 +57,7 @@ function buildPostgrestQuery(filters) { async function listRows(dataQuery, currentState) { const queryOptions = dataQuery.options; const resolvedOptions = resolveReferences(queryOptions, currentState); - if (hasEqualWithNull(resolvedOptions, 'list_rows')) { + if (hasEmptyStringOrNullValue(resolvedOptions, 'list_rows')) { return { status: 'failed', statusText: 'failed', @@ -108,7 +108,7 @@ async function createRow(dataQuery, currentState) { async function updateRows(dataQuery, currentState) { const queryOptions = dataQuery.options; const resolvedOptions = resolveReferences(queryOptions, currentState); - if (hasEqualWithNull(resolvedOptions, 'update_rows')) { + if (hasEmptyStringOrNullValue(resolvedOptions, 'update_rows')) { return { status: 'failed', statusText: 'failed', @@ -136,7 +136,7 @@ async function updateRows(dataQuery, currentState) { async function deleteRows(dataQuery, currentState) { const queryOptions = dataQuery.options; const resolvedOptions = resolveReferences(queryOptions, currentState); - if (hasEqualWithNull(resolvedOptions, 'delete_rows')) { + if (hasEmptyStringOrNullValue(resolvedOptions, 'delete_rows')) { return { status: 'failed', statusText: 'failed', diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/util.js b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/util.js index 777cc19d4b..cc2c86dfbf 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/util.js +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/util.js @@ -3,7 +3,7 @@ import { get } from 'lodash'; /** * Checks if the queryOptions object contains a filter with an 'eq' (equal) operator and a value equal to '{{null}}'. * - * @function hasEqualWithNull + * @function hasEmptyStringOrNullValue * @param {Object} queryOptions - The query options object to check for the presence of the specified filter. * @property {Object} queryOptions.list_rows.where_filters - An object containing the filters to be checked. * @returns {boolean} - Returns true if the specified filter is found, false otherwise. @@ -20,9 +20,9 @@ import { get } from 'lodash'; * }, * }; * - * const result = hasEqualWithNull(queryOptions); // true + * const result = hasEmptyStringOrNullValue(queryOptions); // true */ -export const hasEqualWithNull = (queryOptions, operation) => { +export const hasEmptyStringOrNullValue = (queryOptions, operation) => { const filters = get(queryOptions, `${operation}.where_filters`); if (filters) { const filterKeys = Object.keys(filters); diff --git a/frontend/src/TooljetDatabase/Table/index.jsx b/frontend/src/TooljetDatabase/Table/index.jsx index 93f1ad37cd..4d65b6ca2d 100644 --- a/frontend/src/TooljetDatabase/Table/index.jsx +++ b/frontend/src/TooljetDatabase/Table/index.jsx @@ -98,9 +98,9 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => { () => loading ? columns.map((column) => ({ - ...column, - Cell: , - })) + ...column, + Cell: , + })) : columns, [loading, columns] ); @@ -302,10 +302,11 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => { cell.column.id === 'selection' ? `${cell.row.values?.id}-checkbox` : `id-${cell.row.values?.id}-column-${cell.column.id}`; + const cellValue = cell.value === null ? '' : cell.value return ( Date: Thu, 16 Nov 2023 19:30:35 +0530 Subject: [PATCH 09/25] rename function name for null check --- .../QueryEditors/TooljetDatabase/operations.js | 8 ++++---- .../QueryManager/QueryEditors/TooljetDatabase/util.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js index 96cd3af5b0..d17423a13a 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js @@ -2,7 +2,7 @@ import { tooljetDatabaseService, authenticationService } from '@/_services'; import { isEmpty } from 'lodash'; import PostgrestQueryBuilder from '@/_helpers/postgrestQueryBuilder'; import { resolveReferences } from '@/_helpers/utils'; -import { hasEmptyStringOrNullValue } from './util'; +import { hasNullValueInFilters } from './util'; export const tooljetDbOperations = { perform, @@ -57,7 +57,7 @@ function buildPostgrestQuery(filters) { async function listRows(dataQuery, currentState) { const queryOptions = dataQuery.options; const resolvedOptions = resolveReferences(queryOptions, currentState); - if (hasEmptyStringOrNullValue(resolvedOptions, 'list_rows')) { + if (hasNullValueInFilters(resolvedOptions, 'list_rows')) { return { status: 'failed', statusText: 'failed', @@ -108,7 +108,7 @@ async function createRow(dataQuery, currentState) { async function updateRows(dataQuery, currentState) { const queryOptions = dataQuery.options; const resolvedOptions = resolveReferences(queryOptions, currentState); - if (hasEmptyStringOrNullValue(resolvedOptions, 'update_rows')) { + if (hasNullValueInFilters(resolvedOptions, 'update_rows')) { return { status: 'failed', statusText: 'failed', @@ -136,7 +136,7 @@ async function updateRows(dataQuery, currentState) { async function deleteRows(dataQuery, currentState) { const queryOptions = dataQuery.options; const resolvedOptions = resolveReferences(queryOptions, currentState); - if (hasEmptyStringOrNullValue(resolvedOptions, 'delete_rows')) { + if (hasNullValueInFilters(resolvedOptions, 'delete_rows')) { return { status: 'failed', statusText: 'failed', diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/util.js b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/util.js index cc2c86dfbf..e17035163d 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/util.js +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/util.js @@ -3,7 +3,7 @@ import { get } from 'lodash'; /** * Checks if the queryOptions object contains a filter with an 'eq' (equal) operator and a value equal to '{{null}}'. * - * @function hasEmptyStringOrNullValue + * @function hasNullValueInFilters * @param {Object} queryOptions - The query options object to check for the presence of the specified filter. * @property {Object} queryOptions.list_rows.where_filters - An object containing the filters to be checked. * @returns {boolean} - Returns true if the specified filter is found, false otherwise. @@ -20,9 +20,9 @@ import { get } from 'lodash'; * }, * }; * - * const result = hasEmptyStringOrNullValue(queryOptions); // true + * const result = hasNullValueInFilters(queryOptions); // true */ -export const hasEmptyStringOrNullValue = (queryOptions, operation) => { +export const hasNullValueInFilters = (queryOptions, operation) => { const filters = get(queryOptions, `${operation}.where_filters`); if (filters) { const filterKeys = Object.keys(filters); From d98121cd75f30eaeccc4951267aee06c583ce479 Mon Sep 17 00:00:00 2001 From: Marc Meszaros Date: Thu, 16 Nov 2023 21:17:35 -0800 Subject: [PATCH 10/25] feat: Support multipart/form-data body when file like objects are specified in restapi datasource (#6622) * Automatically convert restapi datasource request body to multipart/form-data if a file object is detected (#6621) * Improve fileobject check function in restapi datasource and guard against bad file data (#6621) * Avoid null or undefined in restapi datasource form-data payload * update keyboard shortcuts page (#8143) --------- Co-authored-by: Karan Rathod Co-authored-by: Akshay --- docs/docs/data-sources/restapi.md | 14 +- docs/docs/tutorial/keyboard-shortcuts.md | 124 +- .../rest-api/multipart-form-data.png | Bin 0 -> 109450 bytes plugins/packages/restapi/lib/index.ts | 48 +- plugins/packages/restapi/package-lock.json | 1217 ++++++++++++----- plugins/packages/restapi/package.json | 1 + 6 files changed, 974 insertions(+), 430 deletions(-) create mode 100644 docs/static/img/datasource-reference/rest-api/multipart-form-data.png diff --git a/docs/docs/data-sources/restapi.md b/docs/docs/data-sources/restapi.md index 04d8057d36..179dfbd11b 100644 --- a/docs/docs/data-sources/restapi.md +++ b/docs/docs/data-sources/restapi.md @@ -1,6 +1,6 @@ --- id: restapi -title: REST API +title: REST API --- ToolJet can establish a connection with any available REST API endpoint and create queries to interact with it. @@ -74,9 +74,19 @@ Whenever a request is made to the REST API, a **tj-x-forwarded-for** header is a
+## Request types + +The plugin will send a **JSON** formatted body by default. If a file object from a [`FilePicker` widget](/docs/widgets/file-picker) is set as a value, the body is automatically converted to be sent as a `multipart/form-data` request. + +
+ +ToolJet - Data source - REST API + +
+ ## Response types -REST APIs can return data in a variety of formats, including **JSON** and **Base64**. JSON is a common format used for data exchange in REST APIs, while Base64 is often used for encoding binary data, such as images or video, within a JSON response. +REST APIs can return data in a variety of formats, including **JSON** and **Base64**. JSON is a common format used for data exchange in REST APIs, while Base64 is often used for encoding binary data, such as images or video, within a JSON response. When the response `content-type` is **image**, the response will be a `base64` string. ### Example JSON response diff --git a/docs/docs/tutorial/keyboard-shortcuts.md b/docs/docs/tutorial/keyboard-shortcuts.md index f534298ff4..26d32d5b93 100644 --- a/docs/docs/tutorial/keyboard-shortcuts.md +++ b/docs/docs/tutorial/keyboard-shortcuts.md @@ -5,118 +5,18 @@ title: Keyboard Shortcuts # Keyboard Shortcuts -You can perform operations like undo, redo, clone, or removing the widget directly using the keyboard shortcuts. +You can perform operations like copying and pasting components, cloning components, deleting components, undo, redo, and more using keyboard shortcuts. -## Copy -You can copy the component on the visual app editor using the following shortcut keys: +| Action | Mac Shortcut | Linux/Windows Shortcut | +|:------------|:-------------------|:-----------------------| +| Copy component | `cmd + c` | `ctrl + c` | +| Cut component | `cmd + x` | `ctrl + x` | +| Paste component | `cmd + v` | `ctrl + v` | +| Undo | `cmd + z` | `ctrl + z` | +| Redo | `cmd + shift + z` | `ctrl + shift + z` | +| Clone component | `cmd + d` | `ctrl + d` | +| Remove component | `delete` | `backspace` | +| Deselect component | `esc` | `esc` | -**On Mac:** `cmd + c` - -**On Linux/Windows:** `ctrl + c` - -
- -Copy - -
- -## Cut - -You can cut the component on the visual app editor using the following shortcut keys: - -**On Mac:** `cmd + x` - -**On Linux/Windows:** `ctrl + x` - -
- -Cut - -
- -## Paste - -You can paste the selected component using the following shortcut keys: - -**On Mac:** `cmd + v` - -**On Linux/Windows:** `ctrl + v` - -
- -Paste - -
- -:::caution -There are few edge cases when copy-paste commands might not work: -- The URL should be `https` and it won't work on http on many browsers -- Recent Firefox versions has some issue with copy functionality -::: - -## Undo - -You can undo any operation performed on the visual app editor using the following shortcut keys: - -**On Mac:** `cmd + z` - -**On Linux/Windows:** `ctrl + z` - -
- -Undo - -
- -## Redo - -If you have `undo` an operation and want to redo that again than you can use the following shortcut keys: - -**On Mac:** `cmd + shift + z` - -**On Linux/Windows:** `ctrl + shift + z` - -
- -Redo - -
- -## Clone - -Now you can create multiple clones of any widget without having to drag and drop the widget again from the sidebar. Just select any widget that you want to create a clone and use the following shortcut keys: - -**On Mac:** `cmd + d` - -**On Linux/Windows:** `ctrl + d` - -
- -Clone - -
- -## Remove widget - -Now you can delete a selected widget by using the following shortcut keys: - -**On Mac:** `delete` - -**On Linux/Windows:** `backspace` - -
- -Remove - -
- -## Unselect the selected widget - -You can quickly deselect a widget using the `esc` key. - -
- -Unselect - -
+To choose several components at once within the app-builder, simply hold down the shift key while clicking on each component you want to select. \ No newline at end of file diff --git a/docs/static/img/datasource-reference/rest-api/multipart-form-data.png b/docs/static/img/datasource-reference/rest-api/multipart-form-data.png new file mode 100644 index 0000000000000000000000000000000000000000..da0bd3dbc0b59565daa10a13acc0c44dc5e49dd8 GIT binary patch literal 109450 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 b9nn3cJoZxPyp8+=_)ker?Lpc7r{MnwQ$6?= literal 0 HcmV?d00001 diff --git a/plugins/packages/restapi/lib/index.ts b/plugins/packages/restapi/lib/index.ts index ed23dbb5ae..7683fc5097 100644 --- a/plugins/packages/restapi/lib/index.ts +++ b/plugins/packages/restapi/lib/index.ts @@ -17,10 +17,26 @@ import { sanitizeSearchParams, getAuthUrl, } from '@tooljet-plugins/common'; +const FormData = require('form-data'); const JSON5 = require('json5'); import got, { HTTPError, OptionsOfTextResponseBody } from 'got'; import { SourceOptions } from './types'; +function isFileObject(value) { + const keys = Object.keys(value); + + return ( + typeof value === 'object' && + keys.length > 0 && + keys.includes('name') && // example.zip + keys.includes('type') && // application/zip + keys.includes('content') && // raw'ish bytes (contains new lines - \n) + keys.includes('dataURL') && // data url representation + keys.includes('base64Data') && // data in base64 + keys.includes('filePath') + ); +} + interface RestAPIResult extends QueryResult { request?: Array | object; response?: Array | object; @@ -83,9 +99,39 @@ export default class RestapiQueryService implements QueryService { ...paramsFromUrl, ...sanitizeSearchParams(sourceOptions, queryOptions, hasDataSource), }, - ...(isUrlEncoded ? { form: json } : { json }), }; + const hasFiles = Object.values(json || {}).some((item) => { + return isFileObject(item); + }); + + if (isUrlEncoded) { + requestOptions.form = json; + } else if (hasFiles) { + const form = new FormData(); + for (const key in json) { + const value = json[key]; + if (isFileObject(value)) { + const fileBuffer = Buffer.from(value?.base64Data || '', 'base64'); + form.append(key, fileBuffer, { + filename: value?.name || '', + contentType: value?.type || '', + knownLength: fileBuffer.length, + }); + } else if (value !== undefined && value !== null) { + form.append(key, value); + } + } + + requestOptions.body = form; + } else { + requestOptions.json = json; + } + + if (authType === 'basic') { + requestOptions.username = sourceOptions.username; + requestOptions.password = sourceOptions.password; + } const authValidatedRequestOptions = validateAndSetRequestOptionsBasedOnAuthType( sourceOptions, context, diff --git a/plugins/packages/restapi/package-lock.json b/plugins/packages/restapi/package-lock.json index bcf533de06..070094962d 100644 --- a/plugins/packages/restapi/package-lock.json +++ b/plugins/packages/restapi/package-lock.json @@ -1,317 +1,904 @@ { - "name": "restapi", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@sindresorhus/is": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", - "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==" - }, - "@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "requires": { - "defer-to-connect": "^2.0.0" - } - }, - "@types/cacheable-request": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", - "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", - "requires": { - "@types/http-cache-semantics": "*", - "@types/keyv": "*", - "@types/node": "*", - "@types/responselike": "*" - } - }, - "@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" - }, - "@types/keyv": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz", - "integrity": "sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==", - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.2.tgz", - "integrity": "sha512-JepeIUPFDARgIs0zD/SKPgFsJEAF0X5/qO80llx59gOxFTboS9Amv3S+QfB7lqBId5sFXJ99BN0J6zFRvL9dDA==" - }, - "@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", - "requires": { - "@types/node": "*" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" - }, - "cacheable-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", - "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - } - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "requires": { - "mimic-response": "^3.1.0" - }, - "dependencies": { - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - } - } - }, - "defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "got": { - "version": "11.8.3", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", - "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", - "requires": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - } - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" - }, - "http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "requires": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "keyv": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", - "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==", - "requires": { - "json-buffer": "3.0.1" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" - }, - "resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" - }, - "responselike": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", - "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", - "requires": { - "lowercase-keys": "^2.0.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } + "name": "@tooljet-plugins/restapi", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@tooljet-plugins/restapi", + "version": "1.0.0", + "dependencies": { + "@tooljet-plugins/common": "file:../common", + "form-data": "^4.0.0", + "got": "^11.8.6", + "react": "^17.0.2", + "rimraf": "^3.0.2", + "url": "^0.11.0" + } + }, + "../common": { + "version": "1.0.0", + "dependencies": { + "react": "^17.0.2", + "rimraf": "^3.0.2" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", + "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tooljet-plugins/common": { + "resolved": "../common", + "link": true + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "node_modules/@types/keyv": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz", + "integrity": "sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.2.tgz", + "integrity": "sha512-JepeIUPFDARgIs0zD/SKPgFsJEAF0X5/qO80llx59gOxFTboS9Amv3S+QfB7lqBId5sFXJ99BN0J6zFRvL9dDA==" + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/keyv": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", + "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + }, + "dependencies": { + "@sindresorhus/is": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", + "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==" + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@tooljet-plugins/common": { + "version": "file:../common", + "requires": { + "react": "^17.0.2", + "rimraf": "^3.0.2" + } + }, + "@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "@types/keyv": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.3.tgz", + "integrity": "sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.2.tgz", + "integrity": "sha512-JepeIUPFDARgIs0zD/SKPgFsJEAF0X5/qO80llx59gOxFTboS9Amv3S+QfB7lqBId5sFXJ99BN0J6zFRvL9dDA==" + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "keyv": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", + "integrity": "sha512-vqNHbAc8BBsxk+7QBYLW0Y219rWcClspR6WSeoHYKG5mnsSoOH+BL1pWq02DDCVdvvuUny5rkBlzMRzoqc+GIg==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } } diff --git a/plugins/packages/restapi/package.json b/plugins/packages/restapi/package.json index fe2d7804cd..4a3f7db3fd 100644 --- a/plugins/packages/restapi/package.json +++ b/plugins/packages/restapi/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@tooljet-plugins/common": "file:../common", + "form-data": "^4.0.0", "got": "^11.8.6", "react": "^17.0.2", "rimraf": "^3.0.2", From 2f48cde33796259bf569efb99263bf86b3ad414d Mon Sep 17 00:00:00 2001 From: Akshay Date: Fri, 17 Nov 2023 12:18:41 +0530 Subject: [PATCH 11/25] Add query validation for TJDB join (#7854) * Updated cypess mysql spec (#7717) * add join table dto * update dto * add join query validation setup * inprogress: join raw query as parameterized query * fix: only limit and offset values are parameterized still filter values are pending to be parameterized * make use of querybuilder with parameterized query * add tjdb orm logging * fix function name * remove unused argument * revise imports * update error message --------- Co-authored-by: Mekhla Asopa <59684099+Mekhla-Asopa@users.noreply.github.com> Co-authored-by: Ganesh Kumar --- .../TooljetDatabase/operations.js | 2 +- frontend/src/HomePage/ExportAppModal.jsx | 484 +++++++++--------- frontend/src/TooljetDatabase/Table/index.jsx | 8 +- server/ormconfig.ts | 1 + .../src/controllers/tooljet_db.controller.ts | 17 +- server/src/dto/tooljet-db-join.dto.ts | 157 ++++++ .../{ => filters}/all-exceptions-filter.ts | 0 .../tooljetdb-join-exceptions-filter.ts | 17 + server/src/main.ts | 2 +- server/src/services/tooljet_db.service.ts | 233 ++++----- 10 files changed, 540 insertions(+), 381 deletions(-) create mode 100644 server/src/dto/tooljet-db-join.dto.ts rename server/src/{ => filters}/all-exceptions-filter.ts (100%) create mode 100644 server/src/filters/tooljetdb-join-exceptions-filter.ts diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js index d17423a13a..82f750a744 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/operations.js @@ -47,7 +47,7 @@ function buildPostgrestQuery(filters) { } if (!isEmpty(column) && !isEmpty(operator)) { - postgrestQueryBuilder[operator](column, value.toString()); + postgrestQueryBuilder[operator](column, value); } } }); diff --git a/frontend/src/HomePage/ExportAppModal.jsx b/frontend/src/HomePage/ExportAppModal.jsx index 7a3566c511..2e33eddaff 100644 --- a/frontend/src/HomePage/ExportAppModal.jsx +++ b/frontend/src/HomePage/ExportAppModal.jsx @@ -6,257 +6,257 @@ import { toast } from 'react-hot-toast'; import { ButtonSolid } from '@/_components/AppButton'; export default function ExportAppModal({ title, show, closeModal, customClassName, app, darkMode }) { - const currentVersion = app?.editing_version; - const [versions, setVersions] = useState(undefined); - const [tables, setTables] = useState(undefined); - const [allTables, setAllTables] = useState(undefined); - const [versionId, setVersionId] = useState(currentVersion?.id); - const [exportTjDb, setExportTjDb] = useState(true); + const currentVersion = app?.editing_version; + const [versions, setVersions] = useState(undefined); + const [tables, setTables] = useState(undefined); + const [allTables, setAllTables] = useState(undefined); + const [versionId, setVersionId] = useState(currentVersion?.id); + const [exportTjDb, setExportTjDb] = useState(true); - - useEffect(() => { - async function fetchAppVersions() { - try { - const fetchVersions = await appsService.getVersions(app.id); - const { versions } = fetchVersions; - setVersions(versions); - } catch (error) { - toast.error('Could not fetch the versions.', { - position: 'top-center', - }); - closeModal(); - } - } - async function fetchAppTables() { - try { - const fetchTables = await appsService.getTables(app.id); // this is used to get all tables - const { tables } = fetchTables; - const tbl = await appsService.getAppByVersion(app.id, versionId) // this is used to get particular App by version - const { dataQueries } = tbl - const extractedIdData = []; - dataQueries.forEach(item => { - if (item.kind === "tooljetdb") { - const joinOptions = item.options?.join_table?.joins ?? []; - (joinOptions || []).forEach((join) => { - const { table, conditions } = join; - if (table) extractedIdData.push(table); - conditions?.conditionsList?.forEach((condition) => { - const { leftField, rightField } = condition; - if (leftField?.table) { - extractedIdData.push(leftField?.table); - } - if (rightField?.table) { - extractedIdData.push(rightField?.table); - } - }); - }); - } - }); - const uniqueSet = new Set(extractedIdData); - const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item })); - setTables(selectedVersiontable) - setAllTables(tables) - } catch (error) { - toast.error('Could not fetch the tables.', { - position: 'top-center', - }); - closeModal(); - } - } - fetchAppVersions(); - fetchAppTables(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [versionId]); - - const exportApp = (app, versionId, exportTjDb, exportTables) => { - const appOpts = { - app: [ - { - id: app.id, - ...(versionId && { search_params: { version_id: versionId } }), - }, - ], - }; - - const requestBody = { - ...appOpts, - ...(exportTjDb && { tooljet_database: exportTables }), - organization_id: app.organization_id, - }; - - appsService - .exportResource(requestBody) - .then((data) => { - const appName = app.name.replace(/\s+/g, '-').toLowerCase(); - const fileName = `${appName}-export-${new Date().getTime()}`; - // simulate link click download - const json = JSON.stringify(data, null, 2); - const blob = new Blob([json], { type: 'application/json' }); - const href = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = href; - link.download = fileName + '.json'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - closeModal(); - }) - .catch((error) => { - toast.error(`Could not export app: ${error.data.message}`, { - position: 'top-center', - }); - closeModal(); + useEffect(() => { + async function fetchAppVersions() { + try { + const fetchVersions = await appsService.getVersions(app.id); + const { versions } = fetchVersions; + setVersions(versions); + } catch (error) { + toast.error('Could not fetch the versions.', { + position: 'top-center', + }); + closeModal(); + } + } + async function fetchAppTables() { + try { + const fetchTables = await appsService.getTables(app.id); // this is used to get all tables + const { tables } = fetchTables; + const tbl = await appsService.getAppByVersion(app.id, versionId); // this is used to get particular App by version + const { dataQueries } = tbl; + const extractedIdData = []; + dataQueries.forEach((item) => { + if (item.kind === 'tooljetdb') { + const joinOptions = item.options?.join_table?.joins ?? []; + (joinOptions || []).forEach((join) => { + const { table, conditions } = join; + if (table) extractedIdData.push(table); + conditions?.conditionsList?.forEach((condition) => { + const { leftField, rightField } = condition; + if (leftField?.table) { + extractedIdData.push(leftField?.table); + } + if (rightField?.table) { + extractedIdData.push(rightField?.table); + } + }); }); + } + }); + const uniqueSet = new Set(extractedIdData); + const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item })); + setTables(selectedVersiontable); + setAllTables(tables); + } catch (error) { + toast.error('Could not fetch the tables.', { + position: 'top-center', + }); + closeModal(); + } + } + fetchAppVersions(); + fetchAppTables(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [versionId]); + + const exportApp = (app, versionId, exportTjDb, exportTables) => { + const appOpts = { + app: [ + { + id: app.id, + ...(versionId && { search_params: { version_id: versionId } }), + }, + ], }; - return ( - closeModal(false)} - contentClassName={`home-modal-component home-version-modal-component ${customClassName ? ` ${customClassName}` : '' - } ${darkMode && 'dark-theme'}`} - show={show} - backdrop={true} - keyboard={true} - enforceFocus={false} - animation={false} - onEscapeKeyDown={() => closeModal()} - centered - data-cy={'modal-component'} - > - - - {title} - - - - {Array.isArray(versions) ? ( - <> - -
-
- - Current Version - - -
- {versions.length >= 2 ? ( -
- - Other Versions - - {versions.map((version) => { - if (version.id !== currentVersion?.id) { - return ( - - ); - } - })} -
- ) : ( -
- No other versions found -
- )} -
-
-
- setExportTjDb(!exportTjDb)} /> -

Export ToolJet table schema

-
- - exportApp(app, null, exportTjDb, allTables)} - > - Export All - - exportApp(app, versionId, exportTjDb, tables)} - > - Export selected version - - - - ) : ( - - )} -
- ); + const requestBody = { + ...appOpts, + ...(exportTjDb && { tooljet_database: exportTables }), + organization_id: app.organization_id, + }; + + appsService + .exportResource(requestBody) + .then((data) => { + const appName = app.name.replace(/\s+/g, '-').toLowerCase(); + const fileName = `${appName}-export-${new Date().getTime()}`; + // simulate link click download + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const href = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.download = fileName + '.json'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + closeModal(); + }) + .catch((error) => { + toast.error(`Could not export app: ${error.data.message}`, { + position: 'top-center', + }); + closeModal(); + }); + }; + + return ( + closeModal(false)} + contentClassName={`home-modal-component home-version-modal-component ${ + customClassName ? ` ${customClassName}` : '' + } ${darkMode && 'dark-theme'}`} + show={show} + backdrop={true} + keyboard={true} + enforceFocus={false} + animation={false} + onEscapeKeyDown={() => closeModal()} + centered + data-cy={'modal-component'} + > + + + {title} + + + + {Array.isArray(versions) ? ( + <> + +
+
+ + Current Version + + +
+ {versions.length >= 2 ? ( +
+ + Other Versions + + {versions.map((version) => { + if (version.id !== currentVersion?.id) { + return ( + + ); + } + })} +
+ ) : ( +
+ No other versions found +
+ )} +
+
+
+ setExportTjDb(!exportTjDb)} /> +

Export ToolJet table schema

+
+ + exportApp(app, null, exportTjDb, allTables)} + > + Export All + + exportApp(app, versionId, exportTjDb, tables)} + > + Export selected version + + + + ) : ( + + )} +
+ ); } function InputRadioField({ - versionId, - versionName, - versionCreatedAt, - checked = undefined, - key = undefined, - setVersionId, - className, + versionId, + versionName, + versionCreatedAt, + checked = undefined, + key = undefined, + setVersionId, + className, }) { - return ( - - setVersionId(target.value)} - style={{ marginLeft: '1rem' }} - className="cursor-pointer" - /> - - - ); + return ( + + setVersionId(target.value)} + style={{ marginLeft: '1rem' }} + className="cursor-pointer" + /> + + + ); } function Loader() { - return ( - -
-
Loading versions ...
-
-
-
- ); + return ( + +
+
Loading versions ...
+
+
+
+ ); } diff --git a/frontend/src/TooljetDatabase/Table/index.jsx b/frontend/src/TooljetDatabase/Table/index.jsx index 4d65b6ca2d..94e990781e 100644 --- a/frontend/src/TooljetDatabase/Table/index.jsx +++ b/frontend/src/TooljetDatabase/Table/index.jsx @@ -98,9 +98,9 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => { () => loading ? columns.map((column) => ({ - ...column, - Cell: , - })) + ...column, + Cell: , + })) : columns, [loading, columns] ); @@ -302,7 +302,7 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => { cell.column.id === 'selection' ? `${cell.row.values?.id}-checkbox` : `id-${cell.row.values?.id}-column-${cell.column.id}`; - const cellValue = cell.value === null ? '' : cell.value + const cellValue = cell.value === null ? '' : cell.value; return ( ability.can(Action.JoinTables, 'all')) - async joinTables(@Body() joinQueryJsonDto: any, @Param('organizationId') organizationId) { + async joinTables(@Body() tooljetDbJoinDto: TooljetDbJoinDto, @Param('organizationId') organizationId) { const params = { - joinQueryJson: { ...joinQueryJsonDto }, + joinQueryJson: { ...tooljetDbJoinDto }, }; const result = await this.tooljetDbService.perform(organizationId, 'join_tables', params); diff --git a/server/src/dto/tooljet-db-join.dto.ts b/server/src/dto/tooljet-db-join.dto.ts new file mode 100644 index 0000000000..299d4a93f1 --- /dev/null +++ b/server/src/dto/tooljet-db-join.dto.ts @@ -0,0 +1,157 @@ +import { IsString, IsArray, ValidateNested, IsIn, IsOptional, IsObject, IsNotEmpty } from 'class-validator'; +import { Type } from 'class-transformer'; + +class Table { + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsNotEmpty() + type: string; +} + +class Field { + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsNotEmpty() + table: string; +} + +class Conditions { + @IsString() + @IsIn(['AND', 'OR']) + @IsOptional() + operator: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ConditionsList) + conditionsList: ConditionsList[]; +} + +class ConditionField { + @IsString() + @IsIn(['Column', 'Value'], { message: 'Condition value not specified' }) + type: string; + + @IsOptional() // present only when type is value + value: unknown; + + @IsString() + @IsOptional() // present only when type is column + table: string; + + @IsString() + @IsOptional() // present only when type is column + columnName: string; +} + +class ConditionsList { + @IsObject() + @IsNotEmpty() + @ValidateNested() + @Type(() => ConditionField) + leftField: ConditionField; + + @IsString() + @IsIn(['=', '>', '>=', '<', '<=', '!=', 'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE', '~', '~*', 'IN', 'NOT IN', 'IS']) + operator: string; + + @IsObject() + @IsNotEmpty() + @ValidateNested() + @Type(() => ConditionField) + rightField: ConditionField; + + @ValidateNested() + @Type(() => Conditions) + @IsOptional() + conditions: Conditions; +} + +class Join { + @IsString() + @IsIn(['INNER', 'LEFT', 'RIGHT', 'FULL OUTER']) + joinType: string; + + @IsString() + @IsNotEmpty() + table: string; + + @ValidateNested() + @IsNotEmpty() + @Type(() => Conditions) + conditions: Conditions; +} + +class GroupBy { + @IsString() + @IsNotEmpty() + table: string; + + @IsString() + @IsNotEmpty() + columnName: string; +} + +class Order { + @IsString() + @IsNotEmpty() + columnName: string; + + @IsString() + @IsNotEmpty() + table: string; + + @IsIn(['ASC', 'DESC']) + @IsNotEmpty() + direction: string; +} + +export class TooljetDbJoinDto { + @ValidateNested() + @Type(() => Table) + @IsNotEmpty() + from: Table; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Field) + @IsNotEmpty() + fields: Field[]; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Join) + @IsNotEmpty() + joins: Join[]; + + @ValidateNested() + @Type(() => Conditions) + @IsOptional() + conditions: Conditions; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => GroupBy) + @IsOptional() + group_by: GroupBy[]; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Order) + @IsOptional() + order_by: Order[]; + + @IsString() + @IsOptional() + limit: string; + + @IsString() + @IsOptional() + offset: string; +} diff --git a/server/src/all-exceptions-filter.ts b/server/src/filters/all-exceptions-filter.ts similarity index 100% rename from server/src/all-exceptions-filter.ts rename to server/src/filters/all-exceptions-filter.ts diff --git a/server/src/filters/tooljetdb-join-exceptions-filter.ts b/server/src/filters/tooljetdb-join-exceptions-filter.ts new file mode 100644 index 0000000000..b76d481bdf --- /dev/null +++ b/server/src/filters/tooljetdb-join-exceptions-filter.ts @@ -0,0 +1,17 @@ +import { Catch, ArgumentsHost, ExceptionFilter, BadRequestException } from '@nestjs/common'; + +@Catch(BadRequestException) +export class TooljetDbJoinExceptionFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost) { + const next = host.switchToHttp().getNext(); + + if (Array.isArray(exception.response.message)) { + const totalErrors = exception.response.message.length; + const firstErrorMessage = exception.response.message[0]; + const strippedErrorMessage = `Error: ${firstErrorMessage} (1/${totalErrors})`; + exception.response.message = strippedErrorMessage; + } + + next(exception); + } +} diff --git a/server/src/main.ts b/server/src/main.ts index 07b828c0b2..2165fb8532 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -7,7 +7,7 @@ import { AppModule } from './app.module'; import * as helmet from 'helmet'; import { Logger } from 'nestjs-pino'; import { urlencoded, json } from 'express'; -import { AllExceptionsFilter } from './all-exceptions-filter'; +import { AllExceptionsFilter } from './filters/all-exceptions-filter'; import { RequestMethod, ValidationPipe, VersioningType, VERSION_NEUTRAL } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { bootstrap as globalAgentBootstrap } from 'global-agent'; diff --git a/server/src/services/tooljet_db.service.ts b/server/src/services/tooljet_db.service.ts index 49dedea3b0..013d856c2c 100644 --- a/server/src/services/tooljet_db.service.ts +++ b/server/src/services/tooljet_db.service.ts @@ -1,9 +1,8 @@ import { BadRequestException, HttpException, Injectable, NotFoundException, Optional } from '@nestjs/common'; -import { EntityManager, In, QueryFailedError } from 'typeorm'; +import { EntityManager, In, ObjectLiteral, QueryFailedError, SelectQueryBuilder, TypeORMError } from 'typeorm'; import { InjectEntityManager } from '@nestjs/typeorm'; import { InternalTable } from 'src/entities/internal_table.entity'; -import { isString, isEmpty } from 'lodash'; -import { PostgrestProxyService } from '@services/postgrest_proxy.service'; +import { isString, isEmpty, camelCase } from 'lodash'; export type TableColumnSchema = { column_name: string; @@ -18,14 +17,32 @@ export type TableColumnSchema = { export type SupportedDataTypes = 'character varying' | 'integer' | 'bigint' | 'serial' | 'double precision' | 'boolean'; +// Patching TypeORM SelectQueryBuilder to handle for right and full outer joins +declare module 'typeorm' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface SelectQueryBuilder { + rightJoin(entityOrProperty: string, alias: string, condition?: string, parameters?: ObjectLiteral): this; + fullOuterJoin(entityOrProperty: string, alias: string, condition?: string, parameters?: ObjectLiteral): this; + } +} + +SelectQueryBuilder.prototype.rightJoin = function (entityOrProperty, alias, condition, parameters) { + this.join('RIGHT', entityOrProperty, alias, condition, parameters); + return this; +}; + +SelectQueryBuilder.prototype.fullOuterJoin = function (entityOrProperty, alias, condition, parameters) { + this.join('FULL OUTER', entityOrProperty, alias, condition, parameters); + return this; +}; + @Injectable() export class TooljetDbService { constructor( private readonly manager: EntityManager, @Optional() @InjectEntityManager('tooljetDb') - private readonly tooljetDbManager: EntityManager, - private readonly postgrestProxyService: PostgrestProxyService + private readonly tooljetDbManager: EntityManager ) {} async perform(organizationId: string, action: string, params = {}) { @@ -290,14 +307,13 @@ export class TooljetDbService { }; }, {}); - const finalQuery = await this.buildJoinQuery(organizationId, joinQueryJson, internalTableIdToNameMap); - try { - return await this.tooljetDbManager.query(finalQuery); + const queryBuilder = this.buildJoinQuery(joinQueryJson, internalTableIdToNameMap); + return await queryBuilder.getRawMany(); } catch (error) { // custom error handling - for Query error - if (error instanceof QueryFailedError) { - let customErrorMessage: string = (error as QueryFailedError).message; + if (error instanceof QueryFailedError || error instanceof TypeORMError) { + let customErrorMessage: string = error.message; Object.entries(internalTableIdToNameMap).forEach(([key, value]) => { customErrorMessage = customErrorMessage.replace(key, value as string); }); @@ -307,137 +323,96 @@ export class TooljetDbService { } } - private async buildJoinQuery(_organizationId: string, queryJson, internalTableIdToNameMap) { - // Pending: For Subquery, Alias is its table name. Need to handle it on Internal Table details mapping - // Pending: SELECT Statement - Nested params --> SUM( price * quantity ) + private buildJoinQuery(queryJson, internalTableIdToNameMap): SelectQueryBuilder { + const queryBuilder: SelectQueryBuilder = this.tooljetDbManager.createQueryBuilder(); - // @description: Only SELECT & FROM statement is Mandatory, else is Optional - let finalQuery = ``; - finalQuery += `SELECT ${await this.constructSelectStatement(queryJson.fields, internalTableIdToNameMap)}`; - finalQuery += `\nFROM ${await this.constructFromStatement(queryJson, internalTableIdToNameMap)}`; - if (queryJson?.joins?.length) - finalQuery += `\n${await this.constructJoinStatements(queryJson.joins, internalTableIdToNameMap)}`; - if ( - queryJson?.conditions && - Object.keys(queryJson?.conditions).length && - queryJson?.conditions?.conditionsList.length - ) - finalQuery += `\nWHERE ${await this.constructWhereStatement(queryJson.conditions, internalTableIdToNameMap)}`; - if (queryJson?.group_by?.length) - finalQuery += `\nGROUP BY ${await this.constructGroupByStatement(queryJson.group_by, internalTableIdToNameMap)}`; - if (queryJson?.having && Object.keys(queryJson?.having).length) - finalQuery += `\nHAVING ${await this.constructWhereStatement(queryJson.having, internalTableIdToNameMap)}`; - if (queryJson?.order_by?.length) - finalQuery += `\nORDER BY ${await this.constructOrderByStatement(queryJson.order_by, internalTableIdToNameMap)}`; - if (queryJson?.limit && queryJson?.limit.length) finalQuery += `\nLIMIT ${queryJson.limit}`; - if (queryJson?.offset && queryJson?.offset.length) finalQuery += `\nOFFSET ${queryJson.offset}`; + // mandatory attributes + if (isEmpty(queryJson.fields)) throw new BadRequestException('Select statement is empty'); + if (isEmpty(queryJson.from)) throw new BadRequestException('From table is not selected'); - return finalQuery; + // select with aliased column names + queryJson.fields.forEach((field) => { + const fieldName = `"${internalTableIdToNameMap[field.table]}"."${field.name}"`; + const fieldAlias = `${internalTableIdToNameMap[field.table]}_${field.name}`; + queryBuilder.addSelect(fieldName, fieldAlias); + }); + + // from table + queryBuilder.from(queryJson.from.name, internalTableIdToNameMap[queryJson.from.name]); + + // join tables with conditions + queryJson.joins.forEach((join) => { + const joinAlias = internalTableIdToNameMap[join.table]; + const conditions = this.constructFilterConditions(join.conditions, internalTableIdToNameMap); + + const joinFunction = queryBuilder[camelCase(join.joinType) + 'Join']; + joinFunction.call(queryBuilder, join.table, joinAlias, conditions.query, conditions.params); + }); + + // conditions + if (queryJson.conditions) { + const conditions = this.constructFilterConditions(queryJson.conditions, internalTableIdToNameMap); + queryBuilder.where(conditions.query, conditions.params); + } + + // order by + if (queryJson.order_by) { + queryJson.order_by.forEach((order) => { + const orderByColumn = `"${internalTableIdToNameMap[order.table]}"."${order.columnName}"`; + queryBuilder.addOrderBy(orderByColumn, order.direction as 'ASC' | 'DESC'); + }); + } + // limit and offset + if (queryJson.limit) queryBuilder.limit(parseInt(queryJson.limit, 10)); + if (queryJson.offset) queryBuilder.offset(parseInt(queryJson.offset, 10)); + + return queryBuilder; } - // Assuming tableId is being passed, tableName to tableId mapping is removed - private constructSelectStatement(selectStatementInputList, internalTableIdToNameMap) { - if (selectStatementInputList.length) { - const selectQueryFields = selectStatementInputList - .map((field) => { - let fieldExpression = ``; - if (field.function) fieldExpression += `${field.function}(`; - fieldExpression += `${field.table ? '"' + field.table + '"' + '.' : ''}${field.name}`; - if (field.function) fieldExpression += `)`; - if (field.alias) { - fieldExpression += ` AS ${field.alias}`; - } else { - // By Default Alias has been added here for tooljetdb join flow - fieldExpression += ` AS ${internalTableIdToNameMap[field.table]}_${field.name}`; + private constructFilterConditions(conditions, internalTableIdToNameMap) { + let conditionString = ''; + const conditionParams = {}; + + const maybeParameterizeValue = (operator, paramName, value) => { + switch (operator) { + case 'IS': + if (value !== 'NULL' && value !== 'NOT NULL') { + throw new BadRequestException('Invalid value for IS operator. Allowed values are NULL or NOT NULL.'); } - return fieldExpression; - }) - .join(', '); - return selectQueryFields; - } + return value; + case 'IN': + if (!Array.isArray(value)) { + throw new BadRequestException('Invalid value for IN operator. Expected an array.'); + } + return `(:...${paramName})`; + default: + return `:${paramName}`; + } + }; - throw new BadRequestException('Select statement is empty'); - } + conditions.conditionsList.forEach((condition, index) => { + const paramName = `${condition.leftField.columnName}_${index}`; - private constructFromStatement(queryJson, _internalTableIdToNameMap) { - const { from } = queryJson; - if (from.name) { - return `${'"' + from.name + '"'} ${from.alias ? from.alias : ''}`; - } + const leftField = + condition.leftField.type == 'Column' + ? `"${internalTableIdToNameMap[condition.leftField.table]}"."${condition.leftField.columnName}"` + : `${condition.leftField.columnName}`; - throw new BadRequestException('From table is not selected'); - } + const rightField = + condition.rightField.type == 'Column' + ? `"${internalTableIdToNameMap[condition.rightField.table]}"."${condition.rightField.columnName}"` + : maybeParameterizeValue(condition.operator, paramName, condition.rightField.value); - private constructJoinStatements(joinsInputList, internalTableIdToNameMap) { - const joinStatementOutput = joinsInputList - .map((joinCondition) => { - const { table, joinType, conditions } = joinCondition; - return `${joinType} JOIN ${'"' + table + '"'} ${ - joinCondition.alias ? joinCondition.alias : '' - } ON ${this.constructWhereStatement(conditions, internalTableIdToNameMap)}`; - }) - .join('\n'); - return joinStatementOutput; - } + conditionString += `${leftField} ${condition.operator} ${rightField}`; - private constructWhereStatement(whereStatementConditions, internalTableIdToNameMap) { - const { operator = 'AND', conditionsList = [] } = whereStatementConditions; - const whereConditionOutput = conditionsList - .map((condition) => { - // @description: Recursive call to build - Sub-condition - if (condition.conditions) - return `(${this.constructWhereStatement(condition.conditions, internalTableIdToNameMap)})`; - // @description: Building a Condition for 'WHERE & HAVING statements' - LHS, operator and RHS - // @description: In LHS & RHS it is not mandatory to provide table name, but column name is mandatory - // @description: In LHS & RHS - We get function only in HAVING statement - const { operator, leftField, rightField } = condition; - // @desc: When 'IS' operator is choosed, 'NULL' & 'NOT NULL' keywords will be provided as value and it should not be converted to string - const keywords = ['NULL', 'NOT NULL']; + conditionParams[paramName] = condition.rightField.value; - let leftSideInput = ``; - if (leftField.type === 'Value') { - const dontAddQuotes = - (keywords.includes(leftField.value) && operator === 'IS') || operator === 'IN' || operator === 'NOT IN'; + if (index < conditions.conditionsList.length - 1) { + conditionString += ` ${conditions.operator} `; + } + }); - leftSideInput += dontAddQuotes ? leftField.value : this.addQuotesIfString(leftField.value); - } else { - if (leftField.function) leftSideInput += `${leftField.function}(`; - leftSideInput += `${leftField.table ? '"' + leftField.table + '"' + '.' : ''}${leftField.columnName}`; - if (leftField.function) leftSideInput += `)`; - } - - let rightSideInput = ``; - if (rightField.type === 'Value') { - const dontAddQuotes = - (keywords.includes(rightField.value) && operator === 'IS') || operator === 'IN' || operator === 'NOT IN'; - - rightSideInput += dontAddQuotes ? rightField.value : this.addQuotesIfString(rightField.value); - } else { - if (rightField.function) rightSideInput += `${rightField.function}(`; - rightSideInput += `${rightField.table ? '"' + rightField.table + '"' + '.' : ''}${rightField.columnName}`; - if (rightField.function) rightSideInput += `)`; - } - - return `${leftSideInput} ${operator} ${rightSideInput}`; - }) - .join(` ${operator} `); - return whereConditionOutput; - } - - private constructGroupByStatement(groupByInputList, _internalTableIdToNameMap) { - return groupByInputList - .map((groupByInput) => `${'"' + groupByInput.table + '"'}.${groupByInput.columnName}`) - .join(', '); - } - - private constructOrderByStatement(orderByInputList, internalTableIdToNameMap) { - // @description: For "ORDER BY" statement table field is optional. But column_name & order_by direction is mandatory - return orderByInputList - .map((orderByInput) => { - const { columnName, direction } = orderByInput; - return `${orderByInput.table ? '"' + orderByInput.table + '"' + '.' : ''}${columnName} ${direction}`; - }) - .join(`, `); + return { query: `(${conditionString})`, params: conditionParams }; } private async findOrFailInternalTableFromTableId(requestedTableIdList: Array, organizationId: string) { From 17ff9f540cb8f31912fe926a62f88f714af68f67 Mon Sep 17 00:00:00 2001 From: Akshay Sasidharan Date: Fri, 17 Nov 2023 12:36:10 +0530 Subject: [PATCH 12/25] explictly check for multipart headers for restapi --- frontend/package-lock.json | 2 ++ plugins/packages/common/lib/index.ts | 2 ++ plugins/packages/common/lib/oauth.ts | 6 ++++++ plugins/packages/restapi/lib/index.ts | 23 +++++++++++------------ 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d8c2c5848d..7710bfdc04 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20295,6 +20295,7 @@ "version": "1.0.0", "dependencies": { "@tooljet-plugins/common": "file:../common", + "form-data": "^4.0.0", "got": "^11.8.6", "react": "^17.0.2", "rimraf": "^3.0.2", @@ -68031,6 +68032,7 @@ "version": "file:../plugins/packages/restapi", "requires": { "@tooljet-plugins/common": "file:../common", + "form-data": "^4.0.0", "got": "^11.8.6", "react": "^17.0.2", "rimraf": "^3.0.2", diff --git a/plugins/packages/common/lib/index.ts b/plugins/packages/common/lib/index.ts index 94ab72e78c..ab03e73bda 100644 --- a/plugins/packages/common/lib/index.ts +++ b/plugins/packages/common/lib/index.ts @@ -20,6 +20,7 @@ import { getAuthUrl, sanitizeCustomParams, checkIfContentTypeIsURLenc, + checkIfContentTypeIsMultipartFormData, validateAndSetRequestOptionsBasedOnAuthType, } from './oauth'; @@ -43,6 +44,7 @@ export { sanitizeHeaders, sanitizeSearchParams, checkIfContentTypeIsURLenc, + checkIfContentTypeIsMultipartFormData, validateAndSetRequestOptionsBasedOnAuthType, fetchHttpsCertsForCustomCA, }; diff --git a/plugins/packages/common/lib/oauth.ts b/plugins/packages/common/lib/oauth.ts index 7d50d92cd4..8c38e97757 100644 --- a/plugins/packages/common/lib/oauth.ts +++ b/plugins/packages/common/lib/oauth.ts @@ -13,6 +13,12 @@ export function checkIfContentTypeIsURLenc(headers: [] = []) { return contentType === 'application/x-www-form-urlencoded'; } +export function checkIfContentTypeIsMultipartFormData(headers: [] = []) { + const objectHeaders = Object.fromEntries(headers); + const contentType = objectHeaders['content-type'] ?? objectHeaders['Content-Type']; + return contentType === 'multipart/form-data'; +} + export function sanitizeCustomParams(customArray: any) { const params = Object.fromEntries(customArray ?? []); Object.keys(params).forEach((key) => (params[key] === '' ? delete params[key] : {})); diff --git a/plugins/packages/restapi/lib/index.ts b/plugins/packages/restapi/lib/index.ts index 7683fc5097..6622fb78fd 100644 --- a/plugins/packages/restapi/lib/index.ts +++ b/plugins/packages/restapi/lib/index.ts @@ -11,6 +11,7 @@ import { OAuthUnauthorizedClientError, getRefreshedToken, checkIfContentTypeIsURLenc, + checkIfContentTypeIsMultipartFormData, isEmpty, validateAndSetRequestOptionsBasedOnAuthType, sanitizeHeaders, @@ -83,6 +84,7 @@ export default class RestapiQueryService implements QueryService { /* REST API queries can be adhoc or associated with a REST API datasource */ const hasDataSource = dataSourceId !== undefined; const isUrlEncoded = checkIfContentTypeIsURLenc(queryOptions['headers']); + const isMultipartFormData = checkIfContentTypeIsMultipartFormData(queryOptions['headers']); /* Prefixing the base url of datasource if datasource exists */ const url = hasDataSource ? `${sourceOptions.url || ''}${queryOptions.url || ''}` : queryOptions.url; @@ -101,13 +103,15 @@ export default class RestapiQueryService implements QueryService { }, }; - const hasFiles = Object.values(json || {}).some((item) => { - return isFileObject(item); - }); + const hasFiles = (json) => { + return Object.values(json || {}).some((item) => { + return isFileObject(item); + }); + }; if (isUrlEncoded) { - requestOptions.form = json; - } else if (hasFiles) { + _requestOptions.form = json; + } else if (isMultipartFormData && hasFiles(json)) { const form = new FormData(); for (const key in json) { const value = json[key]; @@ -122,16 +126,11 @@ export default class RestapiQueryService implements QueryService { form.append(key, value); } } - - requestOptions.body = form; + _requestOptions.body = form; } else { - requestOptions.json = json; + _requestOptions.json = json; } - if (authType === 'basic') { - requestOptions.username = sourceOptions.username; - requestOptions.password = sourceOptions.password; - } const authValidatedRequestOptions = validateAndSetRequestOptionsBasedOnAuthType( sourceOptions, context, From 80dc61e92c38e068de4c2376c7bb98ed20db5021 Mon Sep 17 00:00:00 2001 From: Akshay Sasidharan Date: Fri, 17 Nov 2023 16:06:09 +0530 Subject: [PATCH 13/25] make join errors user friendly --- .../TooljetDatabase/JoinTable.jsx | 1 + server/src/dto/tooljet-db-join.dto.ts | 41 ++++++++++--------- .../tooljetdb-join-exceptions-filter.ts | 2 +- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx index 25cb00b5df..8c40537af3 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/JoinTable.jsx @@ -222,6 +222,7 @@ const RenderFilterSection = ({ darkMode }) => { }; } else { editedFilterCondition = { + operator: 'AND', ...conditions, conditionsList: [...conditionsList, { ...emptyConditionTemplate }], }; diff --git a/server/src/dto/tooljet-db-join.dto.ts b/server/src/dto/tooljet-db-join.dto.ts index 299d4a93f1..82b82c874b 100644 --- a/server/src/dto/tooljet-db-join.dto.ts +++ b/server/src/dto/tooljet-db-join.dto.ts @@ -1,29 +1,31 @@ import { IsString, IsArray, ValidateNested, IsIn, IsOptional, IsObject, IsNotEmpty } from 'class-validator'; import { Type } from 'class-transformer'; +// TODO: We need to remove custom error messages and make use of dto +// default errors and let frontend show the errors on the specific fields class Table { @IsString() - @IsNotEmpty() + @IsNotEmpty({ message: '::Table name for join not selected' }) name: string; @IsString() - @IsNotEmpty() + @IsNotEmpty({ message: '::Table type for join not selected' }) type: string; } class Field { @IsString() - @IsNotEmpty() + @IsNotEmpty({ message: '::Columns names for join not selected' }) name: string; @IsString() - @IsNotEmpty() + @IsNotEmpty({ message: '::Table names for join not selected' }) table: string; } class Conditions { @IsString() - @IsIn(['AND', 'OR']) + @IsIn(['AND', 'OR'], { message: '::Operator for condition not selected (AND | OR)' }) @IsOptional() operator: string; @@ -35,7 +37,7 @@ class Conditions { class ConditionField { @IsString() - @IsIn(['Column', 'Value'], { message: 'Condition value not specified' }) + @IsIn(['Column', 'Value'], { message: '::Condition parameter not specified' }) type: string; @IsOptional() // present only when type is value @@ -52,17 +54,19 @@ class ConditionField { class ConditionsList { @IsObject() - @IsNotEmpty() + @IsNotEmpty({ message: '::Condition value is empty' }) @ValidateNested() @Type(() => ConditionField) leftField: ConditionField; @IsString() - @IsIn(['=', '>', '>=', '<', '<=', '!=', 'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE', '~', '~*', 'IN', 'NOT IN', 'IS']) + @IsIn(['=', '>', '>=', '<', '<=', '!=', 'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE', '~', '~*', 'IN', 'NOT IN', 'IS'], { + message: '::Condition operator not selected', + }) operator: string; @IsObject() - @IsNotEmpty() + @IsNotEmpty({ message: '::Condition value is empty' }) @ValidateNested() @Type(() => ConditionField) rightField: ConditionField; @@ -75,15 +79,15 @@ class ConditionsList { class Join { @IsString() - @IsIn(['INNER', 'LEFT', 'RIGHT', 'FULL OUTER']) + @IsIn(['INNER', 'LEFT', 'RIGHT', 'FULL OUTER'], { message: '::Join type not selected' }) joinType: string; @IsString() - @IsNotEmpty() + @IsNotEmpty({ message: '::Join table is not selected' }) table: string; @ValidateNested() - @IsNotEmpty() + @IsNotEmpty({ message: '::Join condition is not selected' }) @Type(() => Conditions) conditions: Conditions; } @@ -100,34 +104,33 @@ class GroupBy { class Order { @IsString() - @IsNotEmpty() + @IsNotEmpty({ message: '::Sort column not selected' }) columnName: string; @IsString() - @IsNotEmpty() + @IsNotEmpty({ message: '::Sort table not selected' }) table: string; - @IsIn(['ASC', 'DESC']) - @IsNotEmpty() + @IsIn(['ASC', 'DESC'], { message: '::Sort direction not selected' }) direction: string; } export class TooljetDbJoinDto { @ValidateNested() @Type(() => Table) - @IsNotEmpty() + @IsNotEmpty({ message: '::Join table is empty' }) from: Table; @IsArray() @ValidateNested({ each: true }) @Type(() => Field) - @IsNotEmpty() + @IsNotEmpty({ message: '::Join fields are empty' }) fields: Field[]; @IsArray() @ValidateNested({ each: true }) @Type(() => Join) - @IsNotEmpty() + @IsNotEmpty({ message: '::Join parameters are empty' }) joins: Join[]; @ValidateNested() diff --git a/server/src/filters/tooljetdb-join-exceptions-filter.ts b/server/src/filters/tooljetdb-join-exceptions-filter.ts index b76d481bdf..3accbce7c2 100644 --- a/server/src/filters/tooljetdb-join-exceptions-filter.ts +++ b/server/src/filters/tooljetdb-join-exceptions-filter.ts @@ -7,7 +7,7 @@ export class TooljetDbJoinExceptionFilter implements ExceptionFilter { if (Array.isArray(exception.response.message)) { const totalErrors = exception.response.message.length; - const firstErrorMessage = exception.response.message[0]; + const firstErrorMessage = exception.response.message[0].split('::')[1]; const strippedErrorMessage = `Error: ${firstErrorMessage} (1/${totalErrors})`; exception.response.message = strippedErrorMessage; } From 04f98018bed1c57828f10276ba36c759e91e2db2 Mon Sep 17 00:00:00 2001 From: Mekhla Asopa <59684099+Mekhla-Asopa@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:10:48 +0530 Subject: [PATCH 14/25] Updated import spec (#8174) --- .../cypress/constants/selectors/exportImport.js | 2 ++ cypress-tests/cypress/e2e/exportImport/import.cy.js | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cypress-tests/cypress/constants/selectors/exportImport.js b/cypress-tests/cypress/constants/selectors/exportImport.js index 7112bbfc46..5a4be317ea 100644 --- a/cypress-tests/cypress/constants/selectors/exportImport.js +++ b/cypress-tests/cypress/constants/selectors/exportImport.js @@ -39,4 +39,6 @@ export const importSelectors = { importAnApplication: '[data-cy="import-an-application"]', importOptionLabel: '[data-cy="import-option-label"]', importOptionInput: '[data-cy="import-option-input"]', + importAppTitle: '[data-cy="import-app-title"]', + importAppButton: '[data-cy="import-app"]', }; diff --git a/cypress-tests/cypress/e2e/exportImport/import.cy.js b/cypress-tests/cypress/e2e/exportImport/import.cy.js index dab1aebc91..0239820b01 100644 --- a/cypress-tests/cypress/e2e/exportImport/import.cy.js +++ b/cypress-tests/cypress/e2e/exportImport/import.cy.js @@ -66,8 +66,8 @@ describe("App Import Functionality", () => { cy.get(importSelectors.importOptionInput).eq(0).selectFile(appFile, { force: true, }); - cy.get('[data-cy="import-app-title"]').should("be.visible"); - cy.get('[data-cy="Import app"]').click(); + cy.get(importSelectors.importAppTitle).should("be.visible"); + cy.get(importSelectors.importAppButton).click(); cy.get(".go3958317564") .should("be.visible") .and("have.text", importText.appImportedToastMessage); @@ -116,8 +116,8 @@ describe("App Import Functionality", () => { force: true, }); - cy.get('[data-cy="import-app-title"]').should("be.visible"); - cy.get('[data-cy="Import app"]').click(); + cy.get(importSelectors.importAppTitle).should("be.visible"); + cy.get(importSelectors.importAppButton).click(); cy.get(".go3958317564") .should("be.visible") .and("have.text", importText.appImportedToastMessage); @@ -188,8 +188,8 @@ describe("App Import Functionality", () => { force: true, } ); - cy.get('[data-cy="import-app-title"]').should("be.visible"); - cy.get('[data-cy="Import app"]').click(); + cy.get(importSelectors.importAppTitle).should("be.visible"); + cy.get(importSelectors.importAppButton).click(); cy.get(".go3958317564") .should("be.visible") .and("have.text", importText.appImportedToastMessage); From b1e6cba9cee23e7dc8c8c0335a3ff0b7edc8b943 Mon Sep 17 00:00:00 2001 From: Mekhla Asopa <59684099+Mekhla-Asopa@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:14:27 +0530 Subject: [PATCH 15/25] Updated data-cy for bulk update (#7924) * Updated cypess mysql spec (#7717) * Updated data-cy for bulk update --- .../src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx | 7 ++++--- .../src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx b/frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx index 346cc365ec..7a506fb832 100644 --- a/frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx +++ b/frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx @@ -64,9 +64,10 @@ function BulkUploadDrawer({ @@ -79,7 +80,7 @@ function BulkUploadDrawer({ >
-

+

Bulk upload data

@@ -127,7 +128,7 @@ function BulkUploadDrawer({ 0 || errors.server.length > 0} - data-cy={`save-changes-button`} + data-cy={`upload-data-button`} onClick={handleBulkUpload} fill="#fff" leftIcon="floppydisk" diff --git a/frontend/src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx b/frontend/src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx index 564cd10359..3892bcea50 100644 --- a/frontend/src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx +++ b/frontend/src/TooljetDatabase/Drawers/EditRowDrawer/index.jsx @@ -13,6 +13,7 @@ const EditRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) => {