From a5abc70791b79969a53564796453279bb76910cd Mon Sep 17 00:00:00 2001
From: dswbx <dennis.senn@gmx.ch>
Date: Tue, 28 May 2024 15:38:27 +0200
Subject: [PATCH 1/2] Added turso as a data source plugin

---
 server/node-service/src/plugins/index.ts      |   4 +-
 .../node-service/src/plugins/turso/index.ts   | 184 ++++++++++++++++++
 .../src/static/plugin-icons/turso.svg         |  11 ++
 3 files changed, 198 insertions(+), 1 deletion(-)
 create mode 100644 server/node-service/src/plugins/turso/index.ts
 create mode 100644 server/node-service/src/static/plugin-icons/turso.svg

diff --git a/server/node-service/src/plugins/index.ts b/server/node-service/src/plugins/index.ts
index 5944908bd8..62738df2a9 100644
--- a/server/node-service/src/plugins/index.ts
+++ b/server/node-service/src/plugins/index.ts
@@ -33,6 +33,7 @@ import huggingFaceInferencePlugin from "./huggingFaceInference";
 import didPlugin from "./did";
 import bigQueryPlugin from "./bigQuery";
 import appConfigPlugin from "./appconfig";
+import tursoPlugin from "./turso";
 
 let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [
   s3Plugin,
@@ -68,7 +69,8 @@ let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [
   faunaPlugin,
   didPlugin,
   bigQueryPlugin,
-  appConfigPlugin
+  appConfigPlugin,
+  tursoPlugin,
 ];
 
 try {
diff --git a/server/node-service/src/plugins/turso/index.ts b/server/node-service/src/plugins/turso/index.ts
new file mode 100644
index 0000000000..b9d1ed40ce
--- /dev/null
+++ b/server/node-service/src/plugins/turso/index.ts
@@ -0,0 +1,184 @@
+import { DataSourcePlugin, PluginContext } from "lowcoder-sdk/dataSource";
+import _ from "lodash";
+import { ServiceError } from "../../common/error";
+import { ConfigToType } from "lowcoder-sdk/dataSource";
+
+const dataSourceConfig = {
+  type: "dataSource",
+  params: [
+    {
+      key: "url",
+      type: "textInput",
+      label: "URL",
+      tooltip: "e.g. https://db-company.turso.io",
+      placeholder: "<Your Turso URL>",
+      rules: [{ required: true, message: "Please add the URL to your database" }]
+    },
+    {
+      key: "token",
+      label: "Token",
+      type: "password",
+      placeholder: "<Your Token>",
+      rules: [{ required: true, message: "Please input the token" }],
+    }
+  ],
+} as const;
+type DataSourceDataType = ConfigToType<typeof dataSourceConfig>;
+
+const queryConfig = {
+  type: "query",
+  label: "Action",
+  actions: [
+    {
+      actionName: "Query",
+      label: "Query",
+      params: [
+        {
+          label: "Query String",
+          key: "queryString",
+          type: "sqlInput",
+        },
+        {
+          label: "Include raw",
+          key: "includeRaw",
+          tooltip: "Include raw information in the response",
+          type: "switch"
+        }
+      ],
+    },
+  ],
+} as const;
+type ActionDataType = ConfigToType<typeof queryConfig>;
+
+// from https://github.com/tursodatabase/libsql/blob/main/docs/HRANA_3_SPEC.md#hrana-over-http
+type Row = {
+  type: "integer" | "text" | "float" | "blob" | "null";
+  value?: string;
+};
+type Col = {
+  name: string;
+  decltype: string;
+}
+type ResultSet = {
+  cols: Col[];
+  rows: Row[][];
+  affected_row_count: number;
+  last_insert_rowid: number | null;
+  replication_index: string;
+  rows_read: number;
+  rows_written: number;
+  query_duration_ms: number;
+}
+type Result = {
+  type: "ok"
+  response: {
+    type: "execute" | "close" | string;
+    result: ResultSet
+  }
+} | {
+  type: "error";
+  error: any
+};
+
+type Response = {
+  baton: string | null;
+  base_url: string | null;
+  results: Result[];
+}
+
+const tursoPlugin: DataSourcePlugin<ActionDataType, DataSourceDataType> = {
+  id: "turso",
+  name: "Turso",
+  category: "api",
+  icon: "turso.svg",
+  dataSourceConfig,
+  queryConfig,
+  run: async function (actionData, dataSourceConfig, ctx: PluginContext): Promise<any> {
+    const { url: _url, token } = dataSourceConfig;
+    const url = _url.replace("libsql://", "https://");
+    const { queryString, includeRaw } = actionData;
+    
+    const result = await fetch(`${url}/v2/pipeline`, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        "Authorization": `Bearer ${token}`
+      },
+      body: JSON.stringify({
+        requests: [
+          { type: "execute", stmt: { sql: queryString }},
+          { type: "close" }
+        ]
+      })
+    })
+    
+    if (!result.ok) {
+      throw new ServiceError(`Failed to execute query. Endpoint returned ${result.status}: ${result.statusText}.`);
+    }
+    
+    const data = await result.json() as Response;
+    const parsed = parseResult(data.results[0]);
+    
+    return includeRaw ? parsed : parsed?.values;
+  },
+};
+
+
+function parseValue(val: Col & Row): { [key: string]: any } {
+  const name = val.name;
+  let value: any = val.value;
+
+  switch (true) {
+    case val.type === "integer" && val.decltype === "boolean":
+      value = Boolean(Number.parseInt(value));
+      break;
+    case val.type === "integer":
+      value = Number.parseInt(value);
+      break;
+    case val.type === "float":
+      value = Number.parseFloat(value);
+      break;
+    case ["datetime", "date"].includes(val.decltype) && val.type === "text" && !!value:
+      value = new Date(value);
+      break;
+    case val.type === "null":
+      value = null;
+      break;
+  }
+
+  return {
+    [name]: value,
+  };
+}
+
+function parseResult(
+  result: Result
+): { raw: ResultSet; values: Record<string, any> } | undefined {
+  if (result.type === "error") {
+    throw new ServiceError(`Cannot parse result, received error: ${result.error}`);
+  }
+  
+  const res = result.response.result;
+  if (!res) return;
+
+  const combined: (Col & Row)[][] = res.rows.map((row) =>
+    row.map((col, i) => ({
+      ...col,
+      ...res.cols[i],
+    }))
+  );
+
+  const values: any[] = [];
+  for (let row of combined) {
+    values.push(
+      row.reduce((acc, curr) => ({ ...acc, ...parseValue(curr) }), {})
+    );
+  }
+
+  return {
+    values,
+    raw: res,
+  };
+}
+
+export default tursoPlugin;
diff --git a/server/node-service/src/static/plugin-icons/turso.svg b/server/node-service/src/static/plugin-icons/turso.svg
new file mode 100644
index 0000000000..8715e92d5a
--- /dev/null
+++ b/server/node-service/src/static/plugin-icons/turso.svg
@@ -0,0 +1,11 @@
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
+ <g>
+  <title>Layer 1</title>
+  <g stroke="null">
+   <path stroke="null" d="m8.0002,14.33834c-0.16548,0 -1.37445,-0.96646 -1.6084,-1.24249c-0.18438,0.28174 -0.48644,0.60128 -0.48644,0.60128c-0.83451,-0.42082 -1.90083,-1.51496 -2.10197,-1.89798c-0.19793,-0.37767 -0.91654,-4.7264 -0.93579,-5.98922c-0.02568,-0.71076 0.44186,-2.15154 5.1326,-2.15154c4.6911,0 5.15792,1.44078 5.13296,2.15154c-0.0189,1.26282 -0.73715,5.61155 -0.93579,5.98922c-0.20114,0.38302 -1.26746,1.47751 -2.10197,1.89798c0,0 -0.30206,-0.31954 -0.48644,-0.60128c-0.23395,0.27567 -1.44328,1.24249 -1.6084,1.24249l-0.00036,0z" fill="#1EBCA1"/>
+   <path stroke="null" d="m8.00055,11.53809c-1.5656,0 -2.56487,-0.82738 -2.56487,-0.82738l0.14408,-2.01424l-1.64263,-0.14622l-0.29529,-2.38299l8.71815,0l-0.29565,2.38264l-1.64263,0.14657l0.14443,2.01424s-0.99927,0.82738 -2.56487,0.82738l-0.00071,0z" fill="#183134"/>
+   <path stroke="null" d="m9.6225,7.22288l5.93002,-2.05276c-0.35271,-2.11017 -2.20218,-3.67114 -2.20218,-3.67114l0,2.32486l-1.09842,0.28316l-0.68758,-0.82881l-0.58915,1.15904l-2.97428,0.76675l-2.97428,-0.76675l-0.58915,-1.15904l-0.68829,0.82881l-1.0977,-0.28316l0,-2.32486s-1.85126,1.56096 -2.20397,3.67114l5.93038,2.05311l-0.21148,2.82379c0.50606,0.12839 1.0385,0.25606 1.83379,0.25606c0.79528,0 1.32666,-0.12767 1.83307,-0.25606l-0.21148,-2.82379l0.00071,0l0,-0.00036z" fill="#4FF8D2"/>
+  </g>
+ </g>
+
+</svg>
\ No newline at end of file

From 7cadfdd10a2e7ea5a75f0854ba4548bc2933c256 Mon Sep 17 00:00:00 2001
From: dswbx <dennis.senn@gmx.ch>
Date: Tue, 28 May 2024 17:03:43 +0200
Subject: [PATCH 2/2] moved turso to the database section

---
 server/node-service/src/plugins/turso/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/node-service/src/plugins/turso/index.ts b/server/node-service/src/plugins/turso/index.ts
index b9d1ed40ce..418b2a73b7 100644
--- a/server/node-service/src/plugins/turso/index.ts
+++ b/server/node-service/src/plugins/turso/index.ts
@@ -89,7 +89,7 @@ type Response = {
 const tursoPlugin: DataSourcePlugin<ActionDataType, DataSourceDataType> = {
   id: "turso",
   name: "Turso",
-  category: "api",
+  category: "database",
   icon: "turso.svg",
   dataSourceConfig,
   queryConfig,
