Skip to content

Add FirebirdSQL data source #1136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/node-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
"@types/node-fetch": "^2.6.2",
"axios": "^1.2.0",
"base64-arraybuffer": "^1.0.2",
"bluebird": "^3.7.2",
"duckdb-async": "^0.10.0",
"dynamodb-data-types": "^4.0.1",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
Expand All @@ -62,6 +64,7 @@
"lowcoder-sdk": "0.0.41",
"morgan": "^1.10.0",
"node-fetch": "2",
"node-firebird": "^1.1.9",
"openapi-types": "^12.1.0",
"pino": "^8.14.1",
"prom-client": "^14.2.0",
Expand Down
67 changes: 67 additions & 0 deletions server/node-service/src/plugins/firebirdsql/dataSourceConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ConfigToType } from "lowcoder-sdk/dataSource";
import { FirebirdI18nTranslator } from "./i18n";

const getDataSourceConfig = (i18n: FirebirdI18nTranslator) => {
const dataSourceConfig = {
type: "dataSource",
params: [
{
key: "host",
label: "Host Address",
type: "textInput",
placeholder: "<FQDN or IP address>",
rules: [{ required: true, message: i18n.trans("hostRequiredMessage") }],
},
{
key: "database",
label: "Database name",
type: "textInput",
placeholder: "database.fdb",
rules: [{ required: true, message: i18n.trans("dbnameRequiredMessage") }],
},
{
key: "port",
label: i18n.trans("port"),
type: "numberInput",
defaultValue: 3050,
rules: [{ required: true, message: i18n.trans("portRequiredMessage") }],
},
{
key: "username",
label: i18n.trans("username"),
type: "textInput",
defaultValue: "SYSDBA",
rules: [{ required: true, message: i18n.trans("usernameRequiredMessage") }],
},
{
key: "password",
label: i18n.trans("password"),
type: "password",
defaultValue: "masterkey",
},
{
key: "role",
label: i18n.trans("role"),
type: "textInput",
defaultValue: "",
},
{
key: "lowercaseKeys",
label: i18n.trans("lowercaseKeys"),
type: "checkbox",
defaultValue: true,
},
{
key: "blobAsText",
label: i18n.trans("blobAsText"),
type: "checkbox",
defaultValue: true,
},
],
} as const;
return dataSourceConfig;
};

export default getDataSourceConfig;

export type DataSourceDataType = ConfigToType<ReturnType<typeof getDataSourceConfig>>;
19 changes: 19 additions & 0 deletions server/node-service/src/plugins/firebirdsql/i18n/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const en = {
name: "FirebirdSQL",
description: "Support for FirebirdSQL database",
dbnameRequiredMessage: "Please input the database name and/or path",
hostRequiredMessage: "Please input the Host address",
usernameRequiredMessage: "Please input the Username",
portRequiredMessage: "Please specify the Port number",
username: "Username",
password: "Password",
role: "Connection Role",
port: "Port",
lowercaseKeys: "User lowerkeys keys",
blobAsText: "Get blob as text, only affects blob subtype 1",

actions: "Actions",
actionName: "Query",
sqlInputField: "SQL Query",
paramsInputField: "Query Parameters",
};
8 changes: 8 additions & 0 deletions server/node-service/src/plugins/firebirdsql/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { en } from "./en";
import { I18n } from "../../../common/i18n";

export default function getI18nTranslator(languages: string[]) {
return new I18n<typeof en>({ en }, languages);
}

export type FirebirdI18nTranslator = ReturnType<typeof getI18nTranslator>;
37 changes: 37 additions & 0 deletions server/node-service/src/plugins/firebirdsql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DataSourcePluginFactory, PluginContext } from "lowcoder-sdk/dataSource";
import getDataSourceConfig, { DataSourceDataType } from "./dataSourceConfig";
import getQueryConfig, { ActionDataType } from "./queryConfig";
import getI18nTranslator from "./i18n";
import run, { validateDataSourceConfig } from "./run";

const firebirdsqlPlugin: DataSourcePluginFactory = (context: PluginContext) => {
const i18n = getI18nTranslator(context.languages);
return {
id: "firebird",
name: i18n.trans("name"),
icon: "firebirdsql.svg",
description: i18n.trans("description"),
category: "database",
dataSourceConfig: getDataSourceConfig(i18n),
queryConfig: getQueryConfig(i18n),

validateDataSourceConfig: async (dataSourceConfig: DataSourceDataType) => {
return validateDataSourceConfig(dataSourceConfig);
},

run: async (
action: ActionDataType,
dataSourceConfig: DataSourceDataType,
ctx: PluginContext
) => {
const i18n = getI18nTranslator(ctx.languages);
try {
return await run(action, dataSourceConfig, i18n);
} catch (e) {
throw e;
}
},
};
};

export default firebirdsqlPlugin;
32 changes: 32 additions & 0 deletions server/node-service/src/plugins/firebirdsql/queryConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ConfigToType } from "lowcoder-sdk/dataSource";
import { FirebirdI18nTranslator } from "./i18n";

function getQueryConfig(i18n: FirebirdI18nTranslator) {
const queryConfig = {
type: "query",
label: i18n.trans("actions"),
actions: [
{
actionName: "Query",
label: i18n.trans("actionName"),
params: [
{
label: i18n.trans("sqlInputField"),
key: "sql",
type: "sqlInput",
},
{
label: i18n.trans("paramsInputField"),
key: "params",
type: "jsonInput",
},
],
},
],
} as const;
return queryConfig;
}

export type ActionDataType = ConfigToType<ReturnType<typeof getQueryConfig>>;

export default getQueryConfig;
81 changes: 81 additions & 0 deletions server/node-service/src/plugins/firebirdsql/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { DataSourceDataType } from "./dataSourceConfig";
import { ActionDataType } from "./queryConfig";
import { FirebirdI18nTranslator } from "./i18n";
import { ServiceError } from "../../common/error";
import _ from "lodash";
import Firebird from "node-firebird";

// @ts-ignore
import { promisifyAll } from 'bluebird';
const fbdAsync: any = promisifyAll(Firebird);

function getFirebirdOptions(params: DataSourceDataType) {
const options = {
host: params.host.trim(),
port: params.port,
database: params.database.trim(),
user: params.username.trim(),
password: params.password,
lowercase_keys: params.lowercaseKeys, // set to true to lowercase keys
role: _.isEmpty(_.isString(params.role) ? params.role.trim() : null) ? null : params.role.trim(),
pageSize: 4096, // default when creating database
retryConnectionInterval: 1000, // reconnect interval in case of connection drop
blobAsText: params.blobAsText, // set to true to get blob as text, only affects blob subtype 1
encoding: 'UTF8', // default encoding for connection is UTF-8
};
return options;
}

export async function validateDataSourceConfig(dataSourceConfig: DataSourceDataType) {
try {
let db = await fbdAsync.attachAsync(getFirebirdOptions(dataSourceConfig));
promisifyAll(db);
let result = await db.queryAsync("SELECT 1 FROM RDB$DATABASE;");
db.detachAsync();
return {
success: true,
};
} catch (e) {
throw e;
}
}

async function prepareQueryParameters(stmt: string, parameters: object) {
const re : RegExp = /(:)([_a-zA-Z0-9\[\]\.]+)/gm;

const placeholdersInStmt = stmt.matchAll(re);

let parametersArray = [];

for(const match of placeholdersInStmt) {
const paramName: string = match[2];
if (_.isNil(paramName)) {
continue;
}
if (!_.has(parameters, paramName)) {
throw new ServiceError(`Named parameter "${paramName}" not found in Query Parameters object.`);
}
parametersArray.push(_.get(parameters, paramName));
}

const modifiedStmt = stmt.replaceAll(re, "?");

return {
stmt: modifiedStmt,
parametersArray: parametersArray
}
}

export default async function run(action: ActionDataType, dataSourceConfig: DataSourceDataType, i18n: FirebirdI18nTranslator) {
if (action.actionName === "Query") {
let db = await fbdAsync.attachAsync(getFirebirdOptions(dataSourceConfig));
promisifyAll(db);

const { stmt, parametersArray } = await prepareQueryParameters(action.sql, action.params);

const results = db.queryAsync(stmt, parametersArray);

db.detachAsync();
return results;
}
}
2 changes: 2 additions & 0 deletions server/node-service/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import tursoPlugin from "./turso";
import postmanEchoPlugin from "./postmanEcho";
import lowcoderPlugin from "./lowcoder";
import supabaseApiPlugin from "./supabaseApi";
import firebirdsqlPlugin from "./firebirdsql";

let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [

Expand All @@ -46,6 +47,7 @@ let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [
// duckdbPlugin,
faunaPlugin,
tursoPlugin,
firebirdsqlPlugin,

// Big Data
athenaPlugin,
Expand Down
22 changes: 22 additions & 0 deletions server/node-service/src/static/plugin-icons/firebirdsql.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading