feat: MySQL & MSSQL query generator (#24516)

Co-authored-by: Aishwarya UR <aishwarya@appsmith.com>
This commit is contained in:
Sangeeth Sivan 2023-07-06 13:50:58 +05:30 committed by GitHub
parent 820beae5bd
commit f24ecc2473
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1623 additions and 7 deletions

View File

@ -0,0 +1,108 @@
import oneClickBindingLocator from "../../../../../locators/OneClickBindingLocator";
import {
agHelper,
assertHelper,
dataSources,
draggableWidgets,
entityExplorer,
table,
} from "../../../../../support/Objects/ObjectsCore";
import { OneClickBinding } from "../spec_utility";
const oneClickBinding = new OneClickBinding();
// TODO: Adds two rows on click of save row will debug and fix this in a different PR - Sangeeth
describe.skip("Table widget one click binding feature", () => {
it("1.should check that queries are created and bound to table widget properly", () => {
entityExplorer.DragDropWidgetNVerify(draggableWidgets.TABLE, 400);
dataSources.CreateDataSource("MySql");
cy.get("@dsName").then((dsName) => {
entityExplorer.NavigateToSwitcher("Widgets");
entityExplorer.SelectEntityByName("Table1", "Widgets");
oneClickBinding.ChooseAndAssertForm(
`${dsName}`,
dsName,
"configs",
"configName",
);
});
agHelper.GetNClick(oneClickBindingLocator.connectData);
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.Sleep(2000);
[
"id",
"configName",
"configJson",
"configVersion",
"updatedAt",
"updatedBy",
].forEach((column) => {
agHelper.AssertElementExist(table._headerCell(column));
});
agHelper.GetNClick(table._addNewRow, 0, true);
table.EditTableCell(0, 1, "One Click Config", false);
table.UpdateTableCell(0, 2, `{{}"key":"oneClick"}`);
table.UpdateTableCell(0, 3, 36);
table.UpdateTableCell(0, 4, "2023-07-03 15:30:00", false, true);
agHelper.Sleep(2000);
agHelper.GetNClick(table._saveNewRow, 0, true);
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.TypeText(table._searchInput, "One Click Config");
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.AssertElementExist(table._bodyCell("One Click Config"));
agHelper.Sleep(1000);
table.EditTableCell(0, 1, "Bindings", false);
table.EditTableCell(0, 4, "2023-07-03 15:30:00", false);
agHelper.Sleep(1000);
(cy as any).AssertTableRowSavable(6, 0);
(cy as any).saveTableRow(6, 0);
assertHelper.AssertNetworkStatus("@postExecute");
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.Sleep(500);
agHelper.ClearTextField(table._searchInput);
agHelper.TypeText(table._searchInput, "Bindings");
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.Sleep(2000);
agHelper.AssertElementExist(table._bodyCell("Bindings"));
agHelper.ClearTextField(table._searchInput);
agHelper.TypeText(table._searchInput, "One Click Config");
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.Sleep(2000);
agHelper.AssertElementAbsence(table._bodyCell("One Click Config"));
});
});

View File

@ -1,11 +1,18 @@
import {
agHelper,
entityExplorer,
assertHelper,
propPane,
dataSources,
entityItems,
draggableWidgets,
entityExplorer,
table,
} from "../../../support/Objects/ObjectsCore";
import { Widgets } from "../../../support/Pages/DataSources";
import oneClickBindingLocator from "../../../locators/OneClickBindingLocator";
import { OneClickBinding } from "../../Regression/ClientSide/OneClickBinding/spec_utility";
const oneClickBinding = new OneClickBinding();
describe("Validate MsSQL connection & basic querying with UI flows", () => {
let dsName: any,
@ -54,7 +61,8 @@ describe("Validate MsSQL connection & basic querying with UI flows", () => {
dataSources.RunQuery();
query = `CREATE TABLE Simpsons(
episode_id VARCHAR(7) NOT NULL PRIMARY KEY
id INT NOT NULL IDENTITY PRIMARY KEY
,episode_id VARCHAR(7)
,season INTEGER NOT NULL
,episode INTEGER NOT NULL
,number_in_series INTEGER NOT NULL
@ -128,6 +136,102 @@ describe("Validate MsSQL connection & basic querying with UI flows", () => {
});
});
// TODO: This fails with `Invalid Object <tablename>` error. Looks like there needs to be a delay in query exectuion. Will debug and fix this in a different PR - Sangeeth
it.skip("3.One click binding - should check that queries are created and bound to table widget properly", () => {
entityExplorer.DragDropWidgetNVerify(draggableWidgets.TABLE, 450, 200);
oneClickBinding.ChooseAndAssertForm(dsName, dsName, "Simpsons", "title");
agHelper.GetNClick(oneClickBindingLocator.connectData);
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.Sleep(2000);
[
"id",
"episode_id",
"season",
"episode",
"number_in_series",
"title",
"summary",
"air_date",
"episode_image",
"rating",
"votes",
].forEach((column) => {
agHelper.AssertElementExist(table._headerCell(column));
});
agHelper.GetNClick(table._addNewRow, 0, true);
table.EditTableCell(0, 1, "S01E01", false);
table.UpdateTableCell(0, 2, "1");
table.UpdateTableCell(0, 3, " 1");
table.UpdateTableCell(0, 4, " 10");
table.UpdateTableCell(0, 5, "Expanse");
table.UpdateTableCell(0, 6, "Prime");
table.UpdateTableCell(0, 7, "2016-06-22 19:10:25-07", false, true);
agHelper.GetNClick(oneClickBindingLocator.dateInput, 0, true);
agHelper.GetNClick(oneClickBindingLocator.dayViewFromDate, 0, true);
table.UpdateTableCell(0, 8, "expanse.png", false, true);
table.UpdateTableCell(0, 9, "5");
table.UpdateTableCell(0, 10, "20");
agHelper.GetNClick(table._saveNewRow, 0, true, 2000);
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.TypeText(table._searchInput, "Expanse");
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.AssertElementExist(table._bodyCell("Expanse"));
agHelper.Sleep(1000);
table.EditTableCell(0, 5, "Westworld");
agHelper.Sleep(1000);
(cy as any).AssertTableRowSavable(11, 0);
(cy as any).saveTableRow(11, 0);
agHelper.Sleep(2000);
assertHelper.AssertNetworkStatus("@postExecute");
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.Sleep(500);
agHelper.ClearTextField(table._searchInput);
agHelper.TypeText(table._searchInput, "Westworld");
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.Sleep(2000);
agHelper.AssertElementExist(table._bodyCell("Westworld"));
agHelper.ClearTextField(table._searchInput);
agHelper.TypeText(table._searchInput, "Expanse");
assertHelper.AssertNetworkStatus("@postExecute");
agHelper.Sleep(2000);
agHelper.AssertElementAbsence(table._bodyCell("Expanse"));
});
after("Verify Deletion of the datasource", () => {
entityExplorer.SelectEntityByName(dsName, "Datasources");
entityExplorer.ActionContextMenuByEntityName({

View File

@ -1061,11 +1061,11 @@ export class AggregateHelper extends ReusableHelper {
this.Sleep(500); //for value set to settle
}
public UpdateInputValue(selector: string, value: string) {
public UpdateInputValue(selector: string, value: string, force = false) {
this.GetElement(selector)
.closest("input")
.scrollIntoView({ easing: "linear" })
.clear()
.clear({ force })
.then(($input: any) => {
if (value !== "") {
cy.wrap($input).type(value, { delay: 3 });

View File

@ -606,12 +606,14 @@ export class Table {
colIndex: number,
newValue: "" | number | string,
toSaveNewValue = false,
force = false,
) {
this.agHelper.UpdateInputValue(
this._tableRow(rowIndex, colIndex, "v2") +
" " +
this._editCellEditorInput,
newValue.toString(),
force,
);
toSaveNewValue &&
this.agHelper.TypeText(this._editCellEditorInput, "{enter}", 0, true);

View File

@ -0,0 +1,236 @@
import MSSQL from ".";
describe("MSSQL WidgetQueryGenerator", () => {
const initialValues = {
actionConfiguration: {
pluginSpecifiedTemplates: [{ value: true }],
},
};
test("should build select form data correctly", () => {
const expr = MSSQL.build(
{
select: {
limit: "data_table.pageSize",
where: 'data_table.searchText || ""',
offset: "(data_table.pageNo - 1) * data_table.pageSize",
orderBy: "data_table.sortOrder.column",
sortOrder: "data_table.sortOrder.order || 'ASC'",
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: [],
primaryColumn: "genres",
},
initialValues,
);
const res = `SELECT
*
FROM
someTable
WHERE
title LIKE '%{{data_table.searchText || \"\"}}%'
ORDER BY
{{data_table.sortOrder.column || 'genres'}} {{data_table.sortOrder.order || 'ASC' ? \"\" : \"DESC\"}}
OFFSET
{{(data_table.pageNo - 1) * data_table.pageSize}} ROWS
FETCH NEXT
{{data_table.pageSize}} ROWS ONLY`;
expect(expr).toEqual([
{
name: "Select_someTable",
type: "select",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
pluginSpecifiedTemplates: [{ value: false }],
body: res,
},
},
]);
});
test("should build select form data correctly without primary column", () => {
const expr = MSSQL.build(
{
select: {
limit: "data_table.pageSize",
where: 'data_table.searchText || ""',
offset: "(data_table.pageNo - 1) * data_table.pageSize",
orderBy: "data_table.sortOrder.column",
sortOrder: `data_table.sortOrder.order !== "desc"`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: [],
primaryColumn: "",
},
initialValues,
);
const res = `SELECT
*
FROM
someTable
WHERE
title LIKE '%{{data_table.searchText || \"\"}}%' {{data_table.sortOrder.column ? \"ORDER BY \" + data_table.sortOrder.column + \" \" + (data_table.sortOrder.order !== \"desc\" ? \"\" : \"DESC\") : \"\"}}
OFFSET
{{(data_table.pageNo - 1) * data_table.pageSize}} ROWS
FETCH NEXT
{{data_table.pageSize}} ROWS ONLY`;
expect(expr).toEqual([
{
name: "Select_someTable",
type: "select",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
pluginSpecifiedTemplates: [{ value: false }],
body: res,
},
},
]);
});
test("should not build update form data without primary key ", () => {
const expr = MSSQL.build(
{
update: {
value: `update_form.fieldState'`,
where: `"id" = {{data_table.selectedRow.id}}`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "",
},
initialValues,
);
expect(expr).toEqual([]);
});
test("should build update form data correctly ", () => {
const expr = MSSQL.build(
{
update: {
value: `update_form.fieldState'`,
where: `data_table.selectedRow`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "id",
},
initialValues,
);
expect(expr).toEqual([
{
name: "Update_someTable",
type: "update",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
body: "UPDATE someTable SET name= '{{update_form.fieldState'.name}}' WHERE id= '{{data_table.selectedRow.id}}';",
pluginSpecifiedTemplates: [{ value: false }],
},
},
]);
});
test("should not build insert form data without primary key ", () => {
const expr = MSSQL.build(
{
create: {
value: `update_form.fieldState`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
// ignore columns
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "",
},
initialValues,
);
expect(expr).toEqual([]);
});
test("should build insert form data correctly ", () => {
const expr = MSSQL.build(
{
create: {
value: `update_form.fieldState`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
// ignore columns
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "id",
},
initialValues,
);
expect(expr).toEqual([
{
name: "Insert_someTable",
type: "create",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
body: "INSERT INTO someTable (name) VALUES ('{{update_form.fieldState.name}}')",
pluginSpecifiedTemplates: [{ value: false }],
},
},
]);
});
});

View File

@ -0,0 +1,227 @@
import { BaseQueryGenerator } from "../BaseQueryGenerator";
import { format } from "sql-formatter";
import { QUERY_TYPE } from "../types";
import type {
WidgetQueryGenerationConfig,
WidgetQueryGenerationFormConfig,
ActionConfigurationSQL,
} from "../types";
import { removeSpecialChars } from "utils/helpers";
import without from "lodash/without";
export default abstract class MSSQL extends BaseQueryGenerator {
private static buildSelect(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { select } = widgetConfig;
//if no table name do not build query
if (!select || !formConfig.tableName) {
return;
}
const { limit, offset, orderBy, sortOrder, where } = select;
const querySegments = [
{
isValuePresent: formConfig.tableName,
template: "SELECT * FROM ?",
params: [formConfig.tableName],
},
{
isValuePresent: formConfig.searchableColumn && where,
template: "WHERE ? LIKE ?",
params: [formConfig.searchableColumn, `'%{{${where}}}%'`],
},
formConfig.primaryColumn
? {
isValuePresent: orderBy,
template: `ORDER BY ? ?`,
params: [
`{{${orderBy} || '${formConfig.primaryColumn}'}}`,
`{{${sortOrder} ? "" : "DESC"}}`,
],
}
: {
isValuePresent: orderBy,
template: "?",
params: [
`{{${orderBy} ? "ORDER BY " + ${orderBy} + " " + (${sortOrder} ? "" : "DESC") : ""}}`,
],
},
{
isValuePresent: offset,
template: "OFFSET ? ROWS",
params: [`{{${offset}}}`],
},
{
isValuePresent: limit,
template: "FETCH NEXT ? ROWS ONLY",
params: [`{{${limit}}}`],
},
];
const { params, template } = querySegments
// Filter out query segments which are not defined
.filter(({ isValuePresent }) => !!isValuePresent)
.reduce(
(acc, curr) => {
const { params, template } = curr;
return {
template: acc.template + " " + template,
params: [...acc.params, ...params],
};
},
{ template: "", params: [] } as { template: string; params: string[] },
);
//formats sql string
const res = format(template, {
params,
language: "sql",
});
return {
type: QUERY_TYPE.SELECT,
name: `Select_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: res,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildUpdate(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { update } = widgetConfig;
//if no table name do not build query
if (!update || !update.where || !formConfig.tableName) {
return;
}
const { value, where } = update;
const columns = without(formConfig.columns, formConfig.primaryColumn);
return {
type: QUERY_TYPE.UPDATE,
name: `Update_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `UPDATE ${formConfig.tableName} SET ${columns
.map((column) => `${column}= '{{${value}.${column}}}'`)
.join(", ")} WHERE ${formConfig.primaryColumn}= '{{${where}.${
formConfig.primaryColumn
}}}';`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildInsert(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { create } = widgetConfig;
//if no table name do not build query
if (!create || !create.value || !formConfig.tableName) {
return;
}
const columns = without(formConfig.columns, formConfig.primaryColumn);
return {
type: QUERY_TYPE.CREATE,
name: `Insert_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `INSERT INTO ${formConfig.tableName} (${columns.map(
(a) => `${a}`,
)}) VALUES (${columns
.map((d) => `'{{${create.value}.${d}}}'`)
.toString()})`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildTotal(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { select, totalRecord } = widgetConfig;
//if no table name do not build query
if (!totalRecord) {
return;
}
return {
type: QUERY_TYPE.TOTAL_RECORD,
name: `Total_record_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `SELECT COUNT(*) from ${formConfig.tableName}${
formConfig.searchableColumn
? ` WHERE ${formConfig.searchableColumn} LIKE '%{{${select?.where}}}%'`
: ""
};`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
public static build(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
pluginInitalValues: { actionConfiguration: ActionConfigurationSQL },
) {
const allBuildConfigs = [];
if (widgetConfig.select) {
allBuildConfigs.push(this.buildSelect(widgetConfig, formConfig));
}
if (widgetConfig.update && formConfig.primaryColumn) {
allBuildConfigs.push(this.buildUpdate(widgetConfig, formConfig));
}
if (widgetConfig.create && formConfig.primaryColumn) {
allBuildConfigs.push(this.buildInsert(widgetConfig, formConfig));
}
if (widgetConfig.totalRecord) {
allBuildConfigs.push(this.buildTotal(widgetConfig, formConfig));
}
return allBuildConfigs
.filter((val) => !!val)
.map((val) => ({
...val,
payload: {
...(val?.payload || {}),
...(pluginInitalValues?.actionConfiguration || {}),
pluginSpecifiedTemplates: [
{
value: false,
},
],
},
}));
}
static getTotalRecordExpression(binding: string) {
return `${binding}[0].count`;
}
}

View File

@ -0,0 +1,236 @@
import MySQl from ".";
describe("MySQl WidgetQueryGenerator", () => {
const initialValues = {
actionConfiguration: {
pluginSpecifiedTemplates: [{ value: true }],
},
};
test("should build select form data correctly", () => {
const expr = MySQl.build(
{
select: {
limit: "data_table.pageSize",
where: 'data_table.searchText || ""',
offset: "(data_table.pageNo - 1) * data_table.pageSize",
orderBy: "data_table.sortOrder.column",
sortOrder: "data_table.sortOrder.order || 'ASC'",
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: [],
primaryColumn: "genres",
},
initialValues,
);
const res = `SELECT
*
FROM
someTable
WHERE
title LIKE '%{{data_table.searchText || \"\"}}%'
ORDER BY
{{data_table.sortOrder.column || 'genres'}} {{data_table.sortOrder.order || 'ASC' ? \"\" : \"DESC\"}}
LIMIT
{{data_table.pageSize}}
OFFSET
{{(data_table.pageNo - 1) * data_table.pageSize}}`;
expect(expr).toEqual([
{
name: "Select_someTable",
type: "select",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
pluginSpecifiedTemplates: [{ value: false }],
body: res,
},
},
]);
});
test("should build select form data correctly without primary column", () => {
const expr = MySQl.build(
{
select: {
limit: "data_table.pageSize",
where: 'data_table.searchText || ""',
offset: "(data_table.pageNo - 1) * data_table.pageSize",
orderBy: "data_table.sortOrder.column",
sortOrder: `data_table.sortOrder.order !== "desc"`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: [],
primaryColumn: "",
},
initialValues,
);
const res = `SELECT
*
FROM
someTable
WHERE
title LIKE '%{{data_table.searchText || \"\"}}%' {{data_table.sortOrder.column ? \"ORDER BY \" + data_table.sortOrder.column + \" \" + (data_table.sortOrder.order !== \"desc\" ? \"\" : \"DESC\") : \"\"}}
LIMIT
{{data_table.pageSize}}
OFFSET
{{(data_table.pageNo - 1) * data_table.pageSize}}`;
expect(expr).toEqual([
{
name: "Select_someTable",
type: "select",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
pluginSpecifiedTemplates: [{ value: false }],
body: res,
},
},
]);
});
test("should not build update form data without primary key ", () => {
const expr = MySQl.build(
{
update: {
value: `update_form.fieldState'`,
where: `"id" = {{data_table.selectedRow.id}}`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "",
},
initialValues,
);
expect(expr).toEqual([]);
});
test("should build update form data correctly ", () => {
const expr = MySQl.build(
{
update: {
value: `update_form.fieldState'`,
where: `data_table.selectedRow`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "id",
},
initialValues,
);
expect(expr).toEqual([
{
name: "Update_someTable",
type: "update",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
body: "UPDATE someTable SET name= '{{update_form.fieldState'.name}}' WHERE id= '{{data_table.selectedRow.id}}';",
pluginSpecifiedTemplates: [{ value: false }],
},
},
]);
});
test("should not build insert form data without primary key ", () => {
const expr = MySQl.build(
{
create: {
value: `update_form.fieldState`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
// ignore columns
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "",
},
initialValues,
);
expect(expr).toEqual([]);
});
test("should build insert form data correctly ", () => {
const expr = MySQl.build(
{
create: {
value: `update_form.fieldState`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
// ignore columns
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "id",
},
initialValues,
);
expect(expr).toEqual([
{
name: "Insert_someTable",
type: "create",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
body: "INSERT INTO someTable (name) VALUES ('{{update_form.fieldState.name}}')",
pluginSpecifiedTemplates: [{ value: false }],
},
},
]);
});
});

View File

@ -0,0 +1,227 @@
import { BaseQueryGenerator } from "../BaseQueryGenerator";
import { format } from "sql-formatter";
import { QUERY_TYPE } from "../types";
import type {
WidgetQueryGenerationConfig,
WidgetQueryGenerationFormConfig,
ActionConfigurationSQL,
} from "../types";
import { removeSpecialChars } from "utils/helpers";
import without from "lodash/without";
export default abstract class MySQL extends BaseQueryGenerator {
private static buildSelect(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { select } = widgetConfig;
//if no table name do not build query
if (!select || !formConfig.tableName) {
return;
}
const { limit, offset, orderBy, sortOrder, where } = select;
const querySegments = [
{
isValuePresent: formConfig.tableName,
template: "SELECT * FROM ?",
params: [formConfig.tableName],
},
{
isValuePresent: formConfig.searchableColumn && where,
template: "WHERE ? LIKE ?",
params: [formConfig.searchableColumn, `'%{{${where}}}%'`],
},
formConfig.primaryColumn
? {
isValuePresent: orderBy,
template: `ORDER BY ? ?`,
params: [
`{{${orderBy} || '${formConfig.primaryColumn}'}}`,
`{{${sortOrder} ? "" : "DESC"}}`,
],
}
: {
isValuePresent: orderBy,
template: "?",
params: [
`{{${orderBy} ? "ORDER BY " + ${orderBy} + " " + (${sortOrder} ? "" : "DESC") : ""}}`,
],
},
{
isValuePresent: limit,
template: "LIMIT ?",
params: [`{{${limit}}}`],
},
{
isValuePresent: offset,
template: "OFFSET ?",
params: [`{{${offset}}}`],
},
];
const { params, template } = querySegments
// Filter out query segments which are not defined
.filter(({ isValuePresent }) => !!isValuePresent)
.reduce(
(acc, curr) => {
const { params, template } = curr;
return {
template: acc.template + " " + template,
params: [...acc.params, ...params],
};
},
{ template: "", params: [] } as { template: string; params: string[] },
);
//formats sql string
const res = format(template, {
params,
language: "mysql",
});
return {
type: QUERY_TYPE.SELECT,
name: `Select_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: res,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildUpdate(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { update } = widgetConfig;
//if no table name do not build query
if (!update || !update.where || !formConfig.tableName) {
return;
}
const { value, where } = update;
const columns = without(formConfig.columns, formConfig.primaryColumn);
return {
type: QUERY_TYPE.UPDATE,
name: `Update_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `UPDATE ${formConfig.tableName} SET ${columns
.map((column) => `${column}= '{{${value}.${column}}}'`)
.join(", ")} WHERE ${formConfig.primaryColumn}= '{{${where}.${
formConfig.primaryColumn
}}}';`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildInsert(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { create } = widgetConfig;
//if no table name do not build query
if (!create || !create.value || !formConfig.tableName) {
return;
}
const columns = without(formConfig.columns, formConfig.primaryColumn);
return {
type: QUERY_TYPE.CREATE,
name: `Insert_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `INSERT INTO ${formConfig.tableName} (${columns.map(
(a) => `${a}`,
)}) VALUES (${columns
.map((d) => `'{{${create.value}.${d}}}'`)
.toString()})`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildTotal(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { select, totalRecord } = widgetConfig;
//if no table name do not build query
if (!totalRecord) {
return;
}
return {
type: QUERY_TYPE.TOTAL_RECORD,
name: `Total_record_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `SELECT COUNT(*) from ${formConfig.tableName}${
formConfig.searchableColumn
? ` WHERE ${formConfig.searchableColumn} LIKE '%{{${select?.where}}}%'`
: ""
};`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
public static build(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
pluginInitalValues: { actionConfiguration: ActionConfigurationSQL },
) {
const allBuildConfigs = [];
if (widgetConfig.select) {
allBuildConfigs.push(this.buildSelect(widgetConfig, formConfig));
}
if (widgetConfig.update && formConfig.primaryColumn) {
allBuildConfigs.push(this.buildUpdate(widgetConfig, formConfig));
}
if (widgetConfig.create && formConfig.primaryColumn) {
allBuildConfigs.push(this.buildInsert(widgetConfig, formConfig));
}
if (widgetConfig.totalRecord) {
allBuildConfigs.push(this.buildTotal(widgetConfig, formConfig));
}
return allBuildConfigs
.filter((val) => !!val)
.map((val) => ({
...val,
payload: {
...(val?.payload || {}),
...(pluginInitalValues?.actionConfiguration || {}),
pluginSpecifiedTemplates: [
{
value: false,
},
],
},
}));
}
static getTotalRecordExpression(binding: string) {
return `${binding}[0].count`;
}
}

View File

@ -4,7 +4,7 @@ import { QUERY_TYPE } from "../types";
import type {
WidgetQueryGenerationConfig,
WidgetQueryGenerationFormConfig,
ActionConfigurationPostgreSQL,
ActionConfigurationSQL,
} from "../types";
import { removeSpecialChars } from "utils/helpers";
export default abstract class PostgreSQL extends BaseQueryGenerator {
@ -190,7 +190,7 @@ export default abstract class PostgreSQL extends BaseQueryGenerator {
public static build(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
pluginInitalValues: { actionConfiguration: ActionConfigurationPostgreSQL },
pluginInitalValues: { actionConfiguration: ActionConfigurationSQL },
) {
const allBuildConfigs = [];
if (widgetConfig.select) {

View File

@ -0,0 +1,236 @@
import Snowflake from ".";
describe("Snowflake WidgetQueryGenerator", () => {
const initialValues = {
actionConfiguration: {
pluginSpecifiedTemplates: [{ value: true }],
},
};
test("should build select form data correctly", () => {
const expr = Snowflake.build(
{
select: {
limit: "data_table.pageSize",
where: 'data_table.searchText || ""',
offset: "(data_table.pageNo - 1) * data_table.pageSize",
orderBy: "data_table.sortOrder.column",
sortOrder: "data_table.sortOrder.order || 'ASC'",
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: [],
primaryColumn: "genres",
},
initialValues,
);
const res = `SELECT
*
FROM
someTable
WHERE
title LIKE '%{{data_table.searchText || \"\"}}%'
ORDER BY
{{data_table.sortOrder.column || 'genres'}} {{data_table.sortOrder.order || 'ASC' ? \"\" : \"DESC\"}}
LIMIT
{{data_table.pageSize}}
OFFSET
{{(data_table.pageNo - 1) * data_table.pageSize}}`;
expect(expr).toEqual([
{
name: "Select_someTable",
type: "select",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
pluginSpecifiedTemplates: [{ value: false }],
body: res,
},
},
]);
});
test("should build select form data correctly without primary column", () => {
const expr = Snowflake.build(
{
select: {
limit: "data_table.pageSize",
where: 'data_table.searchText || ""',
offset: "(data_table.pageNo - 1) * data_table.pageSize",
orderBy: "data_table.sortOrder.column",
sortOrder: `data_table.sortOrder.order !== "desc"`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: [],
primaryColumn: "",
},
initialValues,
);
const res = `SELECT
*
FROM
someTable
WHERE
title LIKE '%{{data_table.searchText || \"\"}}%' {{data_table.sortOrder.column ? \"ORDER BY \" + data_table.sortOrder.column + \" \" + (data_table.sortOrder.order !== \"desc\" ? \"\" : \"DESC\") : \"\"}}
LIMIT
{{data_table.pageSize}}
OFFSET
{{(data_table.pageNo - 1) * data_table.pageSize}}`;
expect(expr).toEqual([
{
name: "Select_someTable",
type: "select",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
pluginSpecifiedTemplates: [{ value: false }],
body: res,
},
},
]);
});
test("should not build update form data without primary key ", () => {
const expr = Snowflake.build(
{
update: {
value: `update_form.fieldState'`,
where: `"id" = {{data_table.selectedRow.id}}`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "",
},
initialValues,
);
expect(expr).toEqual([]);
});
test("should build update form data correctly ", () => {
const expr = Snowflake.build(
{
update: {
value: `update_form.fieldState'`,
where: `data_table.selectedRow`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "id",
},
initialValues,
);
expect(expr).toEqual([
{
name: "Update_someTable",
type: "update",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
body: "UPDATE someTable SET name= '{{update_form.fieldState'.name}}' WHERE id= '{{data_table.selectedRow.id}}';",
pluginSpecifiedTemplates: [{ value: false }],
},
},
]);
});
test("should not build insert form data without primary key ", () => {
const expr = Snowflake.build(
{
create: {
value: `update_form.fieldState`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
// ignore columns
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "",
},
initialValues,
);
expect(expr).toEqual([]);
});
test("should build insert form data correctly ", () => {
const expr = Snowflake.build(
{
create: {
value: `update_form.fieldState`,
},
totalRecord: false,
},
{
tableName: "someTable",
datasourceId: "someId",
// ignore columns
aliases: [{ name: "someColumn1", alias: "someColumn1" }],
widgetId: "someWidgetId",
searchableColumn: "title",
columns: ["id", "name"],
primaryColumn: "id",
},
initialValues,
);
expect(expr).toEqual([
{
name: "Insert_someTable",
type: "create",
dynamicBindingPathList: [
{
key: "body",
},
],
payload: {
body: "INSERT INTO someTable (name) VALUES ('{{update_form.fieldState.name}}')",
pluginSpecifiedTemplates: [{ value: false }],
},
},
]);
});
});

View File

@ -0,0 +1,231 @@
import { BaseQueryGenerator } from "../BaseQueryGenerator";
import { format } from "sql-formatter";
import { QUERY_TYPE } from "../types";
import type {
WidgetQueryGenerationConfig,
WidgetQueryGenerationFormConfig,
ActionConfigurationSQL,
} from "../types";
import { removeSpecialChars } from "utils/helpers";
import { without } from "workers/common/JSLibrary/lodash-wrapper";
export default abstract class Snowflake extends BaseQueryGenerator {
private static buildSelect(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { select } = widgetConfig;
//if no table name do not build query
if (!select || !formConfig.tableName) {
return;
}
const { limit, offset, orderBy, sortOrder, where } = select;
const querySegments = [
{
isValuePresent: formConfig.tableName,
template: "SELECT * FROM ?",
params: [formConfig.tableName],
},
{
isValuePresent: formConfig.searchableColumn && where,
template: "WHERE ? LIKE ?",
params: [formConfig.searchableColumn, `'%{{${where}}}%'`],
},
formConfig.primaryColumn
? {
isValuePresent: orderBy,
template: `ORDER BY ? ?`,
params: [
`{{${orderBy} || '${formConfig.primaryColumn}'}}`,
`{{${sortOrder} ? "" : "DESC"}}`,
],
}
: {
isValuePresent: orderBy,
template: "?",
params: [
`{{${orderBy} ? "ORDER BY " + ${orderBy} + " " + (${sortOrder} ? "" : "DESC") : ""}}`,
],
},
{
isValuePresent: limit,
template: "LIMIT ?",
params: [`{{${limit}}}`],
},
{
isValuePresent: offset,
template: "OFFSET ?",
params: [`{{${offset}}}`],
},
];
const { params, template } = querySegments
// Filter out query segments which are not defined
.filter(({ isValuePresent }) => !!isValuePresent)
.reduce(
(acc, curr) => {
const { params, template } = curr;
return {
template: acc.template + " " + template,
params: [...acc.params, ...params],
};
},
{ template: "", params: [] } as { template: string; params: string[] },
);
//formats sql string
const res = format(template, {
params,
language: "snowflake",
paramTypes: {
positional: true,
},
});
return {
type: QUERY_TYPE.SELECT,
name: `Select_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: res,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildUpdate(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { update } = widgetConfig;
//if no table name do not build query
if (!update || !update.where || !formConfig.tableName) {
return;
}
const { value, where } = update;
const columns = without(formConfig.columns, formConfig.primaryColumn);
return {
type: QUERY_TYPE.UPDATE,
name: `Update_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `UPDATE ${formConfig.tableName} SET ${columns
.map((column) => `${column}= '{{${value}.${column}}}'`)
.join(", ")} WHERE ${formConfig.primaryColumn}= '{{${where}.${
formConfig.primaryColumn
}}}';`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildInsert(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { create } = widgetConfig;
//if no table name do not build query
if (!create || !create.value || !formConfig.tableName) {
return;
}
const columns = without(formConfig.columns, formConfig.primaryColumn);
return {
type: QUERY_TYPE.CREATE,
name: `Insert_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `INSERT INTO ${formConfig.tableName} (${columns.map(
(a) => `${a}`,
)}) VALUES (${columns
.map((d) => `'{{${create.value}.${d}}}'`)
.toString()})`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
private static buildTotal(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
) {
const { select, totalRecord } = widgetConfig;
//if no table name do not build query
if (!totalRecord) {
return;
}
return {
type: QUERY_TYPE.TOTAL_RECORD,
name: `Total_record_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `SELECT COUNT(*) from ${formConfig.tableName}${
formConfig.searchableColumn
? ` WHERE ${formConfig.searchableColumn} LIKE '%{{${select?.where}}}%'`
: ""
};`,
},
dynamicBindingPathList: [
{
key: "body",
},
],
};
}
public static build(
widgetConfig: WidgetQueryGenerationConfig,
formConfig: WidgetQueryGenerationFormConfig,
pluginInitalValues: { actionConfiguration: ActionConfigurationSQL },
) {
const allBuildConfigs = [];
if (widgetConfig.select) {
allBuildConfigs.push(this.buildSelect(widgetConfig, formConfig));
}
if (widgetConfig.update && formConfig.primaryColumn) {
allBuildConfigs.push(this.buildUpdate(widgetConfig, formConfig));
}
if (widgetConfig.create && formConfig.primaryColumn) {
allBuildConfigs.push(this.buildInsert(widgetConfig, formConfig));
}
if (widgetConfig.totalRecord) {
allBuildConfigs.push(this.buildTotal(widgetConfig, formConfig));
}
return allBuildConfigs
.filter((val) => !!val)
.map((val) => ({
...val,
payload: {
...(val?.payload || {}),
...(pluginInitalValues?.actionConfiguration || {}),
pluginSpecifiedTemplates: [
{
value: false,
},
],
},
}));
}
static getTotalRecordExpression(binding: string) {
return `${binding}[0].count`;
}
}

View File

@ -3,7 +3,13 @@ import WidgetQueryGeneratorRegistry from "utils/WidgetQueryGeneratorRegistry";
import GSheets from "./GSheets";
import MongoDB from "./MongoDB";
import PostgreSQL from "./PostgreSQL";
import MySQL from "./MySQL";
import MsSQL from "./MSSQL";
import Snowflake from "./Snowflake";
WidgetQueryGeneratorRegistry.register(PluginPackageName.MONGO, MongoDB);
WidgetQueryGeneratorRegistry.register(PluginPackageName.POSTGRES, PostgreSQL);
WidgetQueryGeneratorRegistry.register(PluginPackageName.GOOGLE_SHEETS, GSheets);
WidgetQueryGeneratorRegistry.register(PluginPackageName.MY_SQL, MySQL);
WidgetQueryGeneratorRegistry.register(PluginPackageName.MS_SQL, MsSQL);
WidgetQueryGeneratorRegistry.register(PluginPackageName.SNOWFLAKE, Snowflake);

View File

@ -60,7 +60,7 @@ export type ActionConfigurationMongoDB = {
formData: MongoDBFormData;
};
export type ActionConfigurationPostgreSQL = {
export type ActionConfigurationSQL = {
pluginSpecifiedTemplates: Array<object>;
};

View File

@ -23,6 +23,9 @@ export enum PluginPackageName {
GRAPHQL = "graphql-plugin",
JS = "js-plugin",
ORACLE = "oracle-plugin",
MY_SQL = "mysql-plugin",
MS_SQL = "mssql-plugin",
SNOWFLAKE = "snowflake-plugin",
}
// more can be added subsequently.