Skip to content

Commit e7a58b0

Browse files
authored
Merge pull request #914 from dswbx/feature/turso
Added turso as a data source plugin
2 parents 2aef1f3 + bd2d6e1 commit e7a58b0

File tree

3 files changed

+198
-1
lines changed

3 files changed

+198
-1
lines changed

server/node-service/src/plugins/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import huggingFaceInferencePlugin from "./huggingFaceInference";
3333
import didPlugin from "./did";
3434
import bigQueryPlugin from "./bigQuery";
3535
import appConfigPlugin from "./appconfig";
36+
import tursoPlugin from "./turso";
3637

3738
let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [
3839
s3Plugin,
@@ -68,7 +69,8 @@ let plugins: (DataSourcePlugin | DataSourcePluginFactory)[] = [
6869
faunaPlugin,
6970
didPlugin,
7071
bigQueryPlugin,
71-
appConfigPlugin
72+
appConfigPlugin,
73+
tursoPlugin,
7274
];
7375

7476
try {
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { DataSourcePlugin, PluginContext } from "lowcoder-sdk/dataSource";
2+
import _ from "lodash";
3+
import { ServiceError } from "../../common/error";
4+
import { ConfigToType } from "lowcoder-sdk/dataSource";
5+
6+
const dataSourceConfig = {
7+
type: "dataSource",
8+
params: [
9+
{
10+
key: "url",
11+
type: "textInput",
12+
label: "URL",
13+
tooltip: "e.g. https://db-company.turso.io",
14+
placeholder: "<Your Turso URL>",
15+
rules: [{ required: true, message: "Please add the URL to your database" }]
16+
},
17+
{
18+
key: "token",
19+
label: "Token",
20+
type: "password",
21+
placeholder: "<Your Token>",
22+
rules: [{ required: true, message: "Please input the token" }],
23+
}
24+
],
25+
} as const;
26+
type DataSourceDataType = ConfigToType<typeof dataSourceConfig>;
27+
28+
const queryConfig = {
29+
type: "query",
30+
label: "Action",
31+
actions: [
32+
{
33+
actionName: "Query",
34+
label: "Query",
35+
params: [
36+
{
37+
label: "Query String",
38+
key: "queryString",
39+
type: "sqlInput",
40+
},
41+
{
42+
label: "Include raw",
43+
key: "includeRaw",
44+
tooltip: "Include raw information in the response",
45+
type: "switch"
46+
}
47+
],
48+
},
49+
],
50+
} as const;
51+
type ActionDataType = ConfigToType<typeof queryConfig>;
52+
53+
// from https://github.com/tursodatabase/libsql/blob/main/docs/HRANA_3_SPEC.md#hrana-over-http
54+
type Row = {
55+
type: "integer" | "text" | "float" | "blob" | "null";
56+
value?: string;
57+
};
58+
type Col = {
59+
name: string;
60+
decltype: string;
61+
}
62+
type ResultSet = {
63+
cols: Col[];
64+
rows: Row[][];
65+
affected_row_count: number;
66+
last_insert_rowid: number | null;
67+
replication_index: string;
68+
rows_read: number;
69+
rows_written: number;
70+
query_duration_ms: number;
71+
}
72+
type Result = {
73+
type: "ok"
74+
response: {
75+
type: "execute" | "close" | string;
76+
result: ResultSet
77+
}
78+
} | {
79+
type: "error";
80+
error: any
81+
};
82+
83+
type Response = {
84+
baton: string | null;
85+
base_url: string | null;
86+
results: Result[];
87+
}
88+
89+
const tursoPlugin: DataSourcePlugin<ActionDataType, DataSourceDataType> = {
90+
id: "turso",
91+
name: "Turso",
92+
category: "database",
93+
icon: "turso.svg",
94+
dataSourceConfig,
95+
queryConfig,
96+
run: async function (actionData, dataSourceConfig, ctx: PluginContext): Promise<any> {
97+
const { url: _url, token } = dataSourceConfig;
98+
const url = _url.replace("libsql://", "https://");
99+
const { queryString, includeRaw } = actionData;
100+
101+
const result = await fetch(`${url}/v2/pipeline`, {
102+
method: "POST",
103+
headers: {
104+
"Content-Type": "application/json",
105+
"Authorization": `Bearer ${token}`
106+
},
107+
body: JSON.stringify({
108+
requests: [
109+
{ type: "execute", stmt: { sql: queryString }},
110+
{ type: "close" }
111+
]
112+
})
113+
})
114+
115+
if (!result.ok) {
116+
throw new ServiceError(`Failed to execute query. Endpoint returned ${result.status}: ${result.statusText}.`);
117+
}
118+
119+
const data = await result.json() as Response;
120+
const parsed = parseResult(data.results[0]);
121+
122+
return includeRaw ? parsed : parsed?.values;
123+
},
124+
};
125+
126+
127+
function parseValue(val: Col & Row): { [key: string]: any } {
128+
const name = val.name;
129+
let value: any = val.value;
130+
131+
switch (true) {
132+
case val.type === "integer" && val.decltype === "boolean":
133+
value = Boolean(Number.parseInt(value));
134+
break;
135+
case val.type === "integer":
136+
value = Number.parseInt(value);
137+
break;
138+
case val.type === "float":
139+
value = Number.parseFloat(value);
140+
break;
141+
case ["datetime", "date"].includes(val.decltype) && val.type === "text" && !!value:
142+
value = new Date(value);
143+
break;
144+
case val.type === "null":
145+
value = null;
146+
break;
147+
}
148+
149+
return {
150+
[name]: value,
151+
};
152+
}
153+
154+
function parseResult(
155+
result: Result
156+
): { raw: ResultSet; values: Record<string, any> } | undefined {
157+
if (result.type === "error") {
158+
throw new ServiceError(`Cannot parse result, received error: ${result.error}`);
159+
}
160+
161+
const res = result.response.result;
162+
if (!res) return;
163+
164+
const combined: (Col & Row)[][] = res.rows.map((row) =>
165+
row.map((col, i) => ({
166+
...col,
167+
...res.cols[i],
168+
}))
169+
);
170+
171+
const values: any[] = [];
172+
for (let row of combined) {
173+
values.push(
174+
row.reduce((acc, curr) => ({ ...acc, ...parseValue(curr) }), {})
175+
);
176+
}
177+
178+
return {
179+
values,
180+
raw: res,
181+
};
182+
}
183+
184+
export default tursoPlugin;
Lines changed: 11 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)