@telorun/sql 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sql-connection-controller.d.ts +17 -11
- package/dist/sql-connection-controller.js +27 -34
- package/dist/sql-exec-controller.d.ts +1 -1
- package/dist/sql-exec-controller.js +3 -6
- package/dist/sql-postgres-connection-controller.d.ts +19 -0
- package/dist/sql-postgres-connection-controller.js +9 -0
- package/dist/sql-query-controller.js +3 -6
- package/dist/sql-run.d.ts +16 -0
- package/dist/sql-run.js +27 -0
- package/dist/sql-sqlite-connection-controller.d.ts +13 -0
- package/dist/sql-sqlite-connection-controller.js +6 -0
- package/package.json +9 -5
- package/src/sql-connection-controller.ts +45 -50
- package/src/sql-exec-controller.ts +4 -13
- package/src/sql-postgres-connection-controller.ts +28 -0
- package/src/sql-query-controller.ts +3 -13
- package/src/sql-run.ts +49 -0
- package/src/sql-sqlite-connection-controller.ts +18 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import { Kysely, type QueryResult } from "kysely";
|
|
3
3
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
4
|
import type { SqliteDb } from "./sqlite-driver-interface.js";
|
|
@@ -8,30 +8,36 @@ interface PoolConfig {
|
|
|
8
8
|
idleTimeoutMs?: number;
|
|
9
9
|
connectionTimeoutMs?: number;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
export type SqlDriver = "postgres" | "sqlite";
|
|
12
|
+
/** Native bind-placeholder syntax per driver: SQLite binds anonymous `?`,
|
|
13
|
+
* PostgreSQL binds numbered `$1`, `$2`, … */
|
|
14
|
+
export type PlaceholderStyle = "qmark" | "numbered";
|
|
15
|
+
export interface SqlConnectionConfig {
|
|
16
|
+
driver: SqlDriver;
|
|
17
|
+
/** Required for `postgres`; ignored for `sqlite` (which opens via `sqlite`). */
|
|
18
|
+
connectionString?: string;
|
|
17
19
|
pool?: PoolConfig;
|
|
18
20
|
}
|
|
19
|
-
export type SqlDriver = "postgres" | "sqlite";
|
|
20
21
|
export declare class SqlConnectionResource implements ResourceInstance {
|
|
21
22
|
readonly driver: SqlDriver;
|
|
22
23
|
private readonly db;
|
|
23
24
|
private readonly sqlite?;
|
|
24
|
-
constructor(
|
|
25
|
+
constructor(config: SqlConnectionConfig, sqlite?: SqliteDb);
|
|
25
26
|
init(): Promise<void>;
|
|
26
27
|
teardown(): Promise<void>;
|
|
27
28
|
transaction<T>(cb: () => Promise<T>): Promise<T>;
|
|
28
29
|
execute<T>(sql: string, params?: unknown[], transaction?: SqlTransactionResource): Promise<QueryResult<T>>;
|
|
30
|
+
get placeholderStyle(): PlaceholderStyle;
|
|
31
|
+
/** Assemble SQL from literal fragments by interleaving driver-native
|
|
32
|
+
* placeholders, then bind `values` positionally. `fragments.length` must
|
|
33
|
+
* equal `values.length + 1`. */
|
|
34
|
+
executeTemplate<T>(fragments: string[], values: unknown[], transaction?: SqlTransactionResource): Promise<QueryResult<T>>;
|
|
35
|
+
private placeholder;
|
|
29
36
|
executeScript(sql: string): Promise<void>;
|
|
30
37
|
toRowCount(result: QueryResult<unknown>): number;
|
|
31
38
|
get kysely(): Kysely<any>;
|
|
32
39
|
snapshot(): Record<string, unknown>;
|
|
33
40
|
private resolveExecutor;
|
|
34
41
|
}
|
|
35
|
-
export declare function
|
|
36
|
-
export declare function create(resource: SqlConnectionManifest, ctx: ResourceContext): Promise<SqlConnectionResource>;
|
|
42
|
+
export declare function openSqliteDatabase(file?: string): Promise<SqliteDb>;
|
|
37
43
|
export {};
|
|
@@ -6,10 +6,13 @@ export class SqlConnectionResource {
|
|
|
6
6
|
driver;
|
|
7
7
|
db;
|
|
8
8
|
sqlite;
|
|
9
|
-
constructor(
|
|
10
|
-
this.driver =
|
|
9
|
+
constructor(config, sqlite) {
|
|
10
|
+
this.driver = config.driver;
|
|
11
11
|
if (this.driver === "postgres") {
|
|
12
|
-
|
|
12
|
+
if (!config.connectionString) {
|
|
13
|
+
throw new Error("Sql: postgres connection requires a connectionString");
|
|
14
|
+
}
|
|
15
|
+
const url = new URL(config.connectionString);
|
|
13
16
|
const ssl = sslFromSslmode(url.searchParams.get("sslmode"));
|
|
14
17
|
url.searchParams.delete("sslmode");
|
|
15
18
|
this.db = new Kysely({
|
|
@@ -17,10 +20,10 @@ export class SqlConnectionResource {
|
|
|
17
20
|
pool: new Pool({
|
|
18
21
|
connectionString: url.toString(),
|
|
19
22
|
ssl,
|
|
20
|
-
min:
|
|
21
|
-
max:
|
|
22
|
-
idleTimeoutMillis:
|
|
23
|
-
connectionTimeoutMillis:
|
|
23
|
+
min: config.pool?.min ?? 1,
|
|
24
|
+
max: config.pool?.max ?? 10,
|
|
25
|
+
idleTimeoutMillis: config.pool?.idleTimeoutMs,
|
|
26
|
+
connectionTimeoutMillis: config.pool?.connectionTimeoutMs,
|
|
24
27
|
}),
|
|
25
28
|
}),
|
|
26
29
|
});
|
|
@@ -61,6 +64,22 @@ export class SqlConnectionResource {
|
|
|
61
64
|
const executor = this.resolveExecutor(transaction);
|
|
62
65
|
return executor.executeQuery(CompiledQuery.raw(sql, params));
|
|
63
66
|
}
|
|
67
|
+
get placeholderStyle() {
|
|
68
|
+
return this.driver === "postgres" ? "numbered" : "qmark";
|
|
69
|
+
}
|
|
70
|
+
/** Assemble SQL from literal fragments by interleaving driver-native
|
|
71
|
+
* placeholders, then bind `values` positionally. `fragments.length` must
|
|
72
|
+
* equal `values.length + 1`. */
|
|
73
|
+
async executeTemplate(fragments, values, transaction) {
|
|
74
|
+
let sql = fragments[0] ?? "";
|
|
75
|
+
for (let i = 1; i < fragments.length; i++) {
|
|
76
|
+
sql += this.placeholder(i) + fragments[i];
|
|
77
|
+
}
|
|
78
|
+
return this.execute(sql, values, transaction);
|
|
79
|
+
}
|
|
80
|
+
placeholder(index) {
|
|
81
|
+
return this.placeholderStyle === "numbered" ? `$${index}` : "?";
|
|
82
|
+
}
|
|
64
83
|
async executeScript(sql) {
|
|
65
84
|
if (this.driver === "sqlite") {
|
|
66
85
|
this.sqlite?.exec(sql);
|
|
@@ -108,32 +127,6 @@ class TransactionalSqliteDialect extends SqliteDialect {
|
|
|
108
127
|
return new TransactionalSqliteAdapter();
|
|
109
128
|
}
|
|
110
129
|
}
|
|
111
|
-
export function register() { }
|
|
112
|
-
export async function create(resource, ctx) {
|
|
113
|
-
const sqlite = driverFromConnectionString(resource.connectionString) === "sqlite"
|
|
114
|
-
? await openSqliteDatabase(sqliteTargetFromConnectionString(resource.connectionString))
|
|
115
|
-
: undefined;
|
|
116
|
-
return new SqlConnectionResource(resource, sqlite);
|
|
117
|
-
}
|
|
118
|
-
function driverFromConnectionString(connectionString) {
|
|
119
|
-
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(connectionString)?.[1]?.toLowerCase();
|
|
120
|
-
switch (scheme) {
|
|
121
|
-
case "postgres":
|
|
122
|
-
case "postgresql":
|
|
123
|
-
return "postgres";
|
|
124
|
-
case "sqlite":
|
|
125
|
-
return "sqlite";
|
|
126
|
-
default:
|
|
127
|
-
throw new Error(`Sql.Connection: connectionString must start with a driver scheme — ` +
|
|
128
|
-
`'postgres://' or 'postgresql://' for PostgreSQL, 'sqlite:' for SQLite. ` +
|
|
129
|
-
`Got ${scheme ? `'${scheme}:'` : "a string with no scheme"}: ${JSON.stringify(connectionString)}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
function sqliteTargetFromConnectionString(connectionString) {
|
|
133
|
-
const path = decodeURIComponent(new URL(connectionString).pathname);
|
|
134
|
-
// `sqlite:` / `sqlite://` with no path resolves to an in-memory database.
|
|
135
|
-
return path === "" || path === "/" ? ":memory:" : path;
|
|
136
|
-
}
|
|
137
130
|
function sslFromSslmode(mode) {
|
|
138
131
|
switch (mode) {
|
|
139
132
|
case null:
|
|
@@ -152,7 +145,7 @@ function sslFromSslmode(mode) {
|
|
|
152
145
|
`Use 'disable', 'require', 'verify-ca', or 'verify-full'.`);
|
|
153
146
|
}
|
|
154
147
|
}
|
|
155
|
-
async function openSqliteDatabase(file = ":memory:") {
|
|
148
|
+
export async function openSqliteDatabase(file = ":memory:") {
|
|
156
149
|
// Auto-create the parent directory for file-backed databases. SQLite
|
|
157
150
|
// drivers fail-fast when the directory doesn't exist; mirroring `mkdir
|
|
158
151
|
// -p` here lets manifests use paths like `./tmp/foo.sqlite` without a
|
|
@@ -18,7 +18,7 @@ declare class SqlExecResource implements ResourceInstance {
|
|
|
18
18
|
private readonly manifest;
|
|
19
19
|
private readonly ctx;
|
|
20
20
|
constructor(manifest: SqlExecManifest, ctx: ResourceContext);
|
|
21
|
-
invoke(input:
|
|
21
|
+
invoke(input: unknown): Promise<SqlResult>;
|
|
22
22
|
}
|
|
23
23
|
export declare function register(): void;
|
|
24
24
|
export declare function create(resource: SqlExecManifest, ctx: ResourceContext): Promise<SqlExecResource>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
2
|
+
import { runSql } from "./sql-run.js";
|
|
2
3
|
class SqlExecResource {
|
|
3
4
|
manifest;
|
|
4
5
|
ctx;
|
|
@@ -9,18 +10,14 @@ class SqlExecResource {
|
|
|
9
10
|
async invoke(input) {
|
|
10
11
|
const m = this.manifest;
|
|
11
12
|
const ctx = this.ctx;
|
|
12
|
-
const expandedInput = ctx.expandValue(input, {});
|
|
13
13
|
const connection = resolveSqlConnection(m.connection, ctx) ?? m.transaction?.getConnection();
|
|
14
14
|
if (!connection) {
|
|
15
15
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
const result = await runSql(connection, m.transaction, input, ctx);
|
|
18
|
+
return { rows: result.rows, rowCount: connection.toRowCount(result) };
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
async function runExec(connection, transaction, sql, params) {
|
|
21
|
-
const result = await connection.execute(sql, params, transaction);
|
|
22
|
-
return { rows: result.rows, rowCount: connection.toRowCount(result) };
|
|
23
|
-
}
|
|
24
21
|
export function register() { }
|
|
25
22
|
export async function create(resource, ctx) {
|
|
26
23
|
return new SqlExecResource(resource, ctx);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ResourceContext } from "@telorun/sdk";
|
|
2
|
+
import { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
|
+
interface PoolConfig {
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
idleTimeoutMs?: number;
|
|
7
|
+
connectionTimeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
interface PostgresConnectionManifest {
|
|
10
|
+
metadata: {
|
|
11
|
+
name: string;
|
|
12
|
+
module: string;
|
|
13
|
+
};
|
|
14
|
+
connectionString: string;
|
|
15
|
+
pool?: PoolConfig;
|
|
16
|
+
}
|
|
17
|
+
export declare function register(): void;
|
|
18
|
+
export declare function create(resource: PostgresConnectionManifest, ctx: ResourceContext): Promise<SqlConnectionResource>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
2
|
+
export function register() { }
|
|
3
|
+
export async function create(resource, ctx) {
|
|
4
|
+
return new SqlConnectionResource({
|
|
5
|
+
driver: "postgres",
|
|
6
|
+
connectionString: resource.connectionString,
|
|
7
|
+
pool: resource.pool,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
2
|
+
import { runSql } from "./sql-run.js";
|
|
2
3
|
class SqlQueryResource {
|
|
3
4
|
manifest;
|
|
4
5
|
ctx;
|
|
@@ -9,18 +10,14 @@ class SqlQueryResource {
|
|
|
9
10
|
async invoke(input) {
|
|
10
11
|
const m = this.manifest;
|
|
11
12
|
const ctx = this.ctx;
|
|
12
|
-
const expandedInput = ctx.expandValue(input, {});
|
|
13
13
|
const connection = resolveConnection(m.connection, m.transaction, ctx);
|
|
14
|
-
|
|
14
|
+
const result = await runSql(connection, m.transaction, input, ctx);
|
|
15
|
+
return { rows: result.rows, rowCount: result.rows.length };
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
function resolveConnection(connection, transaction, ctx) {
|
|
18
19
|
return (resolveSqlConnection(connection, ctx) ?? transaction?.getConnection() ?? failMissingConnection());
|
|
19
20
|
}
|
|
20
|
-
async function runQuery(connection, transaction, sql, params) {
|
|
21
|
-
const result = await connection.execute(sql, params, transaction);
|
|
22
|
-
return { rows: result.rows, rowCount: result.rows.length };
|
|
23
|
-
}
|
|
24
21
|
function failMissingConnection() {
|
|
25
22
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
26
23
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ResourceContext } from "@telorun/sdk";
|
|
2
|
+
import type { QueryResult } from "kysely";
|
|
3
|
+
import type { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
4
|
+
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
|
+
/** Execute the `sql` input of a Query/Exec resource against `connection`.
|
|
6
|
+
*
|
|
7
|
+
* Two modes:
|
|
8
|
+
* - **inline (`!sql` tag):** the expanded `sql` is a parameterized template —
|
|
9
|
+
* each `${{ }}` is bound via the connection's native placeholder, never
|
|
10
|
+
* spliced into the text.
|
|
11
|
+
* - **escape hatch (`bindings` given):** `sql` is a literal string with
|
|
12
|
+
* author-written `?` / `$n` placeholders and `bindings` bound positionally.
|
|
13
|
+
*
|
|
14
|
+
* Mixing a `!sql` template with `bindings` is rejected. A plain string `sql`
|
|
15
|
+
* with neither is executed verbatim. */
|
|
16
|
+
export declare function runSql(connection: SqlConnectionResource, transaction: SqlTransactionResource | undefined, input: unknown, ctx: ResourceContext): Promise<QueryResult<Record<string, unknown>>>;
|
package/dist/sql-run.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { InvokeError, isParameterizedSql } from "@telorun/sdk";
|
|
2
|
+
/** Execute the `sql` input of a Query/Exec resource against `connection`.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - **inline (`!sql` tag):** the expanded `sql` is a parameterized template —
|
|
6
|
+
* each `${{ }}` is bound via the connection's native placeholder, never
|
|
7
|
+
* spliced into the text.
|
|
8
|
+
* - **escape hatch (`bindings` given):** `sql` is a literal string with
|
|
9
|
+
* author-written `?` / `$n` placeholders and `bindings` bound positionally.
|
|
10
|
+
*
|
|
11
|
+
* Mixing a `!sql` template with `bindings` is rejected. A plain string `sql`
|
|
12
|
+
* with neither is executed verbatim. */
|
|
13
|
+
export async function runSql(connection, transaction, input, ctx) {
|
|
14
|
+
const expanded = ctx.expandValue(input, {});
|
|
15
|
+
const sql = expanded.sql;
|
|
16
|
+
const bindings = expanded.bindings;
|
|
17
|
+
const hasBindings = bindings !== undefined && bindings !== null;
|
|
18
|
+
if (isParameterizedSql(sql)) {
|
|
19
|
+
if (hasBindings) {
|
|
20
|
+
throw new InvokeError("ERR_INVALID_INPUT", "Sql: a `!sql` template cannot be combined with an explicit `bindings` array. " +
|
|
21
|
+
"Use one or the other — `!sql` binds each inline value automatically; " +
|
|
22
|
+
"`bindings` is for hand-written ? / $n placeholders.");
|
|
23
|
+
}
|
|
24
|
+
return connection.executeTemplate(sql.fragments, sql.values, transaction);
|
|
25
|
+
}
|
|
26
|
+
return connection.execute(sql, hasBindings ? bindings : [], transaction);
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResourceContext } from "@telorun/sdk";
|
|
2
|
+
import { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
|
+
interface SqliteConnectionManifest {
|
|
4
|
+
metadata: {
|
|
5
|
+
name: string;
|
|
6
|
+
module: string;
|
|
7
|
+
};
|
|
8
|
+
/** File path, or omitted / `:memory:` for an in-memory database. */
|
|
9
|
+
file?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function register(): void;
|
|
12
|
+
export declare function create(resource: SqliteConnectionManifest, ctx: ResourceContext): Promise<SqlConnectionResource>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { openSqliteDatabase, SqlConnectionResource } from "./sql-connection-controller.js";
|
|
2
|
+
export function register() { }
|
|
3
|
+
export async function create(resource, ctx) {
|
|
4
|
+
const sqlite = await openSqliteDatabase(resource.file ?? ":memory:");
|
|
5
|
+
return new SqlConnectionResource({ driver: "sqlite" }, sqlite);
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -22,9 +22,13 @@
|
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
24
|
"exports": {
|
|
25
|
-
"./sql-connection": {
|
|
26
|
-
"bun": "./src/sql-connection-controller.ts",
|
|
27
|
-
"import": "./dist/sql-connection-controller.js"
|
|
25
|
+
"./sql-postgres-connection": {
|
|
26
|
+
"bun": "./src/sql-postgres-connection-controller.ts",
|
|
27
|
+
"import": "./dist/sql-postgres-connection-controller.js"
|
|
28
|
+
},
|
|
29
|
+
"./sql-sqlite-connection": {
|
|
30
|
+
"bun": "./src/sql-sqlite-connection-controller.ts",
|
|
31
|
+
"import": "./dist/sql-sqlite-connection-controller.js"
|
|
28
32
|
},
|
|
29
33
|
"./sql-query": {
|
|
30
34
|
"bun": "./src/sql-query-controller.ts",
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
"@types/node": "^20.0.0",
|
|
71
75
|
"@types/pg": "^8.0.0",
|
|
72
76
|
"typescript": "^5.0.0",
|
|
73
|
-
"@telorun/sdk": "0.
|
|
77
|
+
"@telorun/sdk": "0.21.0"
|
|
74
78
|
},
|
|
75
79
|
"peerDependencies": {
|
|
76
80
|
"@telorun/sdk": "*"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import {
|
|
4
4
|
CompiledQuery,
|
|
@@ -21,24 +21,32 @@ interface PoolConfig {
|
|
|
21
21
|
connectionTimeoutMs?: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
export type SqlDriver = "postgres" | "sqlite";
|
|
25
|
+
|
|
26
|
+
/** Native bind-placeholder syntax per driver: SQLite binds anonymous `?`,
|
|
27
|
+
* PostgreSQL binds numbered `$1`, `$2`, … */
|
|
28
|
+
export type PlaceholderStyle = "qmark" | "numbered";
|
|
29
|
+
|
|
30
|
+
export interface SqlConnectionConfig {
|
|
31
|
+
driver: SqlDriver;
|
|
32
|
+
/** Required for `postgres`; ignored for `sqlite` (which opens via `sqlite`). */
|
|
33
|
+
connectionString?: string;
|
|
27
34
|
pool?: PoolConfig;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
export type SqlDriver = "postgres" | "sqlite";
|
|
31
|
-
|
|
32
37
|
export class SqlConnectionResource implements ResourceInstance {
|
|
33
38
|
readonly driver: SqlDriver;
|
|
34
39
|
private readonly db: Kysely<any>;
|
|
35
40
|
private readonly sqlite?: SqliteDb;
|
|
36
41
|
|
|
37
|
-
constructor(
|
|
38
|
-
this.driver =
|
|
42
|
+
constructor(config: SqlConnectionConfig, sqlite?: SqliteDb) {
|
|
43
|
+
this.driver = config.driver;
|
|
39
44
|
|
|
40
45
|
if (this.driver === "postgres") {
|
|
41
|
-
|
|
46
|
+
if (!config.connectionString) {
|
|
47
|
+
throw new Error("Sql: postgres connection requires a connectionString");
|
|
48
|
+
}
|
|
49
|
+
const url = new URL(config.connectionString);
|
|
42
50
|
const ssl = sslFromSslmode(url.searchParams.get("sslmode"));
|
|
43
51
|
url.searchParams.delete("sslmode");
|
|
44
52
|
this.db = new Kysely({
|
|
@@ -46,10 +54,10 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
46
54
|
pool: new Pool({
|
|
47
55
|
connectionString: url.toString(),
|
|
48
56
|
ssl,
|
|
49
|
-
min:
|
|
50
|
-
max:
|
|
51
|
-
idleTimeoutMillis:
|
|
52
|
-
connectionTimeoutMillis:
|
|
57
|
+
min: config.pool?.min ?? 1,
|
|
58
|
+
max: config.pool?.max ?? 10,
|
|
59
|
+
idleTimeoutMillis: config.pool?.idleTimeoutMs,
|
|
60
|
+
connectionTimeoutMillis: config.pool?.connectionTimeoutMs,
|
|
53
61
|
}),
|
|
54
62
|
}),
|
|
55
63
|
});
|
|
@@ -98,6 +106,29 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
98
106
|
return executor.executeQuery<T>(CompiledQuery.raw(sql, params));
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
get placeholderStyle(): PlaceholderStyle {
|
|
110
|
+
return this.driver === "postgres" ? "numbered" : "qmark";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Assemble SQL from literal fragments by interleaving driver-native
|
|
114
|
+
* placeholders, then bind `values` positionally. `fragments.length` must
|
|
115
|
+
* equal `values.length + 1`. */
|
|
116
|
+
async executeTemplate<T>(
|
|
117
|
+
fragments: string[],
|
|
118
|
+
values: unknown[],
|
|
119
|
+
transaction?: SqlTransactionResource,
|
|
120
|
+
): Promise<QueryResult<T>> {
|
|
121
|
+
let sql = fragments[0] ?? "";
|
|
122
|
+
for (let i = 1; i < fragments.length; i++) {
|
|
123
|
+
sql += this.placeholder(i) + fragments[i];
|
|
124
|
+
}
|
|
125
|
+
return this.execute<T>(sql, values, transaction);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private placeholder(index: number): string {
|
|
129
|
+
return this.placeholderStyle === "numbered" ? `$${index}` : "?";
|
|
130
|
+
}
|
|
131
|
+
|
|
101
132
|
async executeScript(sql: string): Promise<void> {
|
|
102
133
|
if (this.driver === "sqlite") {
|
|
103
134
|
this.sqlite?.exec(sql);
|
|
@@ -156,46 +187,10 @@ class TransactionalSqliteDialect extends SqliteDialect {
|
|
|
156
187
|
}
|
|
157
188
|
}
|
|
158
189
|
|
|
159
|
-
export function register(): void {}
|
|
160
|
-
|
|
161
|
-
export async function create(
|
|
162
|
-
resource: SqlConnectionManifest,
|
|
163
|
-
ctx: ResourceContext,
|
|
164
|
-
): Promise<SqlConnectionResource> {
|
|
165
|
-
const sqlite =
|
|
166
|
-
driverFromConnectionString(resource.connectionString) === "sqlite"
|
|
167
|
-
? await openSqliteDatabase(sqliteTargetFromConnectionString(resource.connectionString))
|
|
168
|
-
: undefined;
|
|
169
|
-
return new SqlConnectionResource(resource, sqlite);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
190
|
type SslOption =
|
|
173
191
|
| false
|
|
174
192
|
| { rejectUnauthorized: boolean; checkServerIdentity?: () => undefined };
|
|
175
193
|
|
|
176
|
-
function driverFromConnectionString(connectionString: string): SqlDriver {
|
|
177
|
-
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(connectionString)?.[1]?.toLowerCase();
|
|
178
|
-
switch (scheme) {
|
|
179
|
-
case "postgres":
|
|
180
|
-
case "postgresql":
|
|
181
|
-
return "postgres";
|
|
182
|
-
case "sqlite":
|
|
183
|
-
return "sqlite";
|
|
184
|
-
default:
|
|
185
|
-
throw new Error(
|
|
186
|
-
`Sql.Connection: connectionString must start with a driver scheme — ` +
|
|
187
|
-
`'postgres://' or 'postgresql://' for PostgreSQL, 'sqlite:' for SQLite. ` +
|
|
188
|
-
`Got ${scheme ? `'${scheme}:'` : "a string with no scheme"}: ${JSON.stringify(connectionString)}`,
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function sqliteTargetFromConnectionString(connectionString: string): string {
|
|
194
|
-
const path = decodeURIComponent(new URL(connectionString).pathname);
|
|
195
|
-
// `sqlite:` / `sqlite://` with no path resolves to an in-memory database.
|
|
196
|
-
return path === "" || path === "/" ? ":memory:" : path;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
194
|
function sslFromSslmode(mode: string | null): SslOption {
|
|
200
195
|
switch (mode) {
|
|
201
196
|
case null:
|
|
@@ -217,7 +212,7 @@ function sslFromSslmode(mode: string | null): SslOption {
|
|
|
217
212
|
}
|
|
218
213
|
}
|
|
219
214
|
|
|
220
|
-
async function openSqliteDatabase(file = ":memory:"): Promise<SqliteDb> {
|
|
215
|
+
export async function openSqliteDatabase(file = ":memory:"): Promise<SqliteDb> {
|
|
221
216
|
// Auto-create the parent directory for file-backed databases. SQLite
|
|
222
217
|
// drivers fail-fast when the directory doesn't exist; mirroring `mkdir
|
|
223
218
|
// -p` here lets manifests use paths like `./tmp/foo.sqlite` without a
|
|
@@ -2,6 +2,7 @@ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
|
2
2
|
import type { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
4
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
5
|
+
import { runSql } from "./sql-run.js";
|
|
5
6
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
6
7
|
|
|
7
8
|
interface SqlExecManifest {
|
|
@@ -20,30 +21,20 @@ class SqlExecResource implements ResourceInstance {
|
|
|
20
21
|
private readonly ctx: ResourceContext,
|
|
21
22
|
) {}
|
|
22
23
|
|
|
23
|
-
async invoke(input:
|
|
24
|
+
async invoke(input: unknown): Promise<SqlResult> {
|
|
24
25
|
const m = this.manifest;
|
|
25
26
|
const ctx = this.ctx;
|
|
26
|
-
const expandedInput = ctx.expandValue(input, {});
|
|
27
27
|
|
|
28
28
|
const connection = resolveSqlConnection(m.connection, ctx) ?? m.transaction?.getConnection();
|
|
29
29
|
if (!connection) {
|
|
30
30
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const result = await runSql(connection, m.transaction, input, ctx);
|
|
34
|
+
return { rows: result.rows, rowCount: connection.toRowCount(result) };
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
async function runExec(
|
|
38
|
-
connection: SqlConnectionResource,
|
|
39
|
-
transaction: SqlTransactionResource | undefined,
|
|
40
|
-
sql: string,
|
|
41
|
-
params: unknown[],
|
|
42
|
-
): Promise<SqlResult> {
|
|
43
|
-
const result = await connection.execute<Record<string, unknown>>(sql, params, transaction);
|
|
44
|
-
return { rows: result.rows, rowCount: connection.toRowCount(result) };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
38
|
export function register(): void {}
|
|
48
39
|
|
|
49
40
|
export async function create(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResourceContext } from "@telorun/sdk";
|
|
2
|
+
import { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
|
+
|
|
4
|
+
interface PoolConfig {
|
|
5
|
+
min?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
idleTimeoutMs?: number;
|
|
8
|
+
connectionTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PostgresConnectionManifest {
|
|
12
|
+
metadata: { name: string; module: string };
|
|
13
|
+
connectionString: string;
|
|
14
|
+
pool?: PoolConfig;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function register(): void {}
|
|
18
|
+
|
|
19
|
+
export async function create(
|
|
20
|
+
resource: PostgresConnectionManifest,
|
|
21
|
+
ctx: ResourceContext,
|
|
22
|
+
): Promise<SqlConnectionResource> {
|
|
23
|
+
return new SqlConnectionResource({
|
|
24
|
+
driver: "postgres",
|
|
25
|
+
connectionString: resource.connectionString,
|
|
26
|
+
pool: resource.pool,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
|
+
import { runSql } from "./sql-run.js";
|
|
4
5
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
6
|
|
|
6
7
|
interface SqlQueryManifest {
|
|
@@ -27,10 +28,9 @@ class SqlQueryResource implements ResourceInstance {
|
|
|
27
28
|
async invoke(input: unknown): Promise<SqlResult> {
|
|
28
29
|
const m = this.manifest;
|
|
29
30
|
const ctx = this.ctx;
|
|
30
|
-
const expandedInput = ctx.expandValue(input, {});
|
|
31
|
-
|
|
32
31
|
const connection = resolveConnection(m.connection, m.transaction, ctx);
|
|
33
|
-
|
|
32
|
+
const result = await runSql(connection, m.transaction, input, ctx);
|
|
33
|
+
return { rows: result.rows, rowCount: result.rows.length };
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -44,16 +44,6 @@ function resolveConnection(
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async function runQuery(
|
|
48
|
-
connection: SqlConnectionResource,
|
|
49
|
-
transaction: SqlTransactionResource | undefined,
|
|
50
|
-
sql: string,
|
|
51
|
-
params: unknown[],
|
|
52
|
-
): Promise<SqlResult> {
|
|
53
|
-
const result = await connection.execute<Record<string, unknown>>(sql, params, transaction);
|
|
54
|
-
return { rows: result.rows, rowCount: result.rows.length };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
47
|
function failMissingConnection(): never {
|
|
58
48
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
59
49
|
}
|
package/src/sql-run.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { InvokeError, isParameterizedSql, type ResourceContext } from "@telorun/sdk";
|
|
2
|
+
import type { QueryResult } from "kysely";
|
|
3
|
+
import type { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
4
|
+
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
|
+
|
|
6
|
+
/** Execute the `sql` input of a Query/Exec resource against `connection`.
|
|
7
|
+
*
|
|
8
|
+
* Two modes:
|
|
9
|
+
* - **inline (`!sql` tag):** the expanded `sql` is a parameterized template —
|
|
10
|
+
* each `${{ }}` is bound via the connection's native placeholder, never
|
|
11
|
+
* spliced into the text.
|
|
12
|
+
* - **escape hatch (`bindings` given):** `sql` is a literal string with
|
|
13
|
+
* author-written `?` / `$n` placeholders and `bindings` bound positionally.
|
|
14
|
+
*
|
|
15
|
+
* Mixing a `!sql` template with `bindings` is rejected. A plain string `sql`
|
|
16
|
+
* with neither is executed verbatim. */
|
|
17
|
+
export async function runSql(
|
|
18
|
+
connection: SqlConnectionResource,
|
|
19
|
+
transaction: SqlTransactionResource | undefined,
|
|
20
|
+
input: unknown,
|
|
21
|
+
ctx: ResourceContext,
|
|
22
|
+
): Promise<QueryResult<Record<string, unknown>>> {
|
|
23
|
+
const expanded = ctx.expandValue(input, {}) as { sql: unknown; bindings?: unknown[] };
|
|
24
|
+
const sql = expanded.sql;
|
|
25
|
+
const bindings = expanded.bindings;
|
|
26
|
+
const hasBindings = bindings !== undefined && bindings !== null;
|
|
27
|
+
|
|
28
|
+
if (isParameterizedSql(sql)) {
|
|
29
|
+
if (hasBindings) {
|
|
30
|
+
throw new InvokeError(
|
|
31
|
+
"ERR_INVALID_INPUT",
|
|
32
|
+
"Sql: a `!sql` template cannot be combined with an explicit `bindings` array. " +
|
|
33
|
+
"Use one or the other — `!sql` binds each inline value automatically; " +
|
|
34
|
+
"`bindings` is for hand-written ? / $n placeholders.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return connection.executeTemplate<Record<string, unknown>>(
|
|
38
|
+
sql.fragments,
|
|
39
|
+
sql.values,
|
|
40
|
+
transaction,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return connection.execute<Record<string, unknown>>(
|
|
45
|
+
sql as string,
|
|
46
|
+
hasBindings ? (bindings as unknown[]) : [],
|
|
47
|
+
transaction,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResourceContext } from "@telorun/sdk";
|
|
2
|
+
import { openSqliteDatabase, SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
|
+
|
|
4
|
+
interface SqliteConnectionManifest {
|
|
5
|
+
metadata: { name: string; module: string };
|
|
6
|
+
/** File path, or omitted / `:memory:` for an in-memory database. */
|
|
7
|
+
file?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function register(): void {}
|
|
11
|
+
|
|
12
|
+
export async function create(
|
|
13
|
+
resource: SqliteConnectionManifest,
|
|
14
|
+
ctx: ResourceContext,
|
|
15
|
+
): Promise<SqlConnectionResource> {
|
|
16
|
+
const sqlite = await openSqliteDatabase(resource.file ?? ":memory:");
|
|
17
|
+
return new SqlConnectionResource({ driver: "sqlite" }, sqlite);
|
|
18
|
+
}
|