@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.
@@ -1,4 +1,4 @@
1
- import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
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
- interface SqlConnectionManifest {
12
- metadata: {
13
- name: string;
14
- module: string;
15
- };
16
- connectionString: string;
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(m: SqlConnectionManifest, sqlite?: SqliteDb);
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 register(): void;
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(m, sqlite) {
10
- this.driver = driverFromConnectionString(m.connectionString);
9
+ constructor(config, sqlite) {
10
+ this.driver = config.driver;
11
11
  if (this.driver === "postgres") {
12
- const url = new URL(m.connectionString);
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: m.pool?.min ?? 1,
21
- max: m.pool?.max ?? 10,
22
- idleTimeoutMillis: m.pool?.idleTimeoutMs,
23
- connectionTimeoutMillis: m.pool?.connectionTimeoutMs,
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: any): Promise<SqlResult>;
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
- return runExec(connection, m.transaction, expandedInput.sql, expandedInput.bindings ?? []);
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
- return runQuery(connection, m.transaction, expandedInput.sql, expandedInput.bindings);
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>>>;
@@ -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.6.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.19.0"
77
+ "@telorun/sdk": "0.21.0"
74
78
  },
75
79
  "peerDependencies": {
76
80
  "@telorun/sdk": "*"
@@ -1,4 +1,4 @@
1
- import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
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
- interface SqlConnectionManifest {
25
- metadata: { name: string; module: string };
26
- connectionString: string;
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(m: SqlConnectionManifest, sqlite?: SqliteDb) {
38
- this.driver = driverFromConnectionString(m.connectionString);
42
+ constructor(config: SqlConnectionConfig, sqlite?: SqliteDb) {
43
+ this.driver = config.driver;
39
44
 
40
45
  if (this.driver === "postgres") {
41
- const url = new URL(m.connectionString);
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: m.pool?.min ?? 1,
50
- max: m.pool?.max ?? 10,
51
- idleTimeoutMillis: m.pool?.idleTimeoutMs,
52
- connectionTimeoutMillis: m.pool?.connectionTimeoutMs,
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: any): Promise<SqlResult> {
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
- return runExec(connection, m.transaction, expandedInput.sql, expandedInput.bindings ?? []);
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
- return runQuery(connection, m.transaction, expandedInput.sql, expandedInput.bindings);
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
+ }