@telorun/sql 0.7.2 → 0.9.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/README.md +2 -141
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/{sql-exec-controller.d.ts → sql-command-controller.d.ts} +4 -4
- package/dist/{sql-exec-controller.js → sql-command-controller.js} +4 -3
- package/dist/sql-connection-controller.d.ts +15 -15
- package/dist/sql-connection-controller.js +20 -88
- package/dist/sql-connection-ref.d.ts +10 -7
- package/dist/sql-connection-ref.js +13 -22
- package/dist/sql-migrations-controller.js +1 -1
- package/dist/sql-query-controller.js +5 -3
- package/dist/{sql-select-controller.d.ts → sql-selection-controller.d.ts} +2 -2
- package/dist/{sql-select-controller.js → sql-selection-controller.js} +6 -5
- package/dist/sql-transaction-controller.js +1 -2
- package/package.json +19 -23
- package/src/index.ts +8 -0
- package/src/{sql-exec-controller.ts → sql-command-controller.ts} +9 -7
- package/src/sql-connection-controller.ts +27 -121
- package/src/sql-connection-ref.ts +14 -33
- package/src/sql-migrations-controller.ts +5 -1
- package/src/sql-query-controller.ts +10 -2
- package/src/{sql-select-controller.ts → sql-selection-controller.ts} +8 -6
- package/src/sql-transaction-controller.ts +5 -2
- package/dist/sql-postgres-connection-controller.d.ts +0 -19
- package/dist/sql-postgres-connection-controller.js +0 -9
- package/dist/sql-sqlite-connection-controller.d.ts +0 -13
- package/dist/sql-sqlite-connection-controller.js +0 -6
- package/dist/sqlite-driver-bun.d.ts +0 -2
- package/dist/sqlite-driver-bun.js +0 -31
- package/dist/sqlite-driver-node.d.ts +0 -2
- package/dist/sqlite-driver-node.js +0 -31
- package/src/sql-postgres-connection-controller.ts +0 -28
- package/src/sql-sqlite-connection-controller.ts +0 -18
- package/src/sqlite-driver-bun.ts +0 -34
- package/src/sqlite-driver-node.ts +0 -35
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Built to be language-agnostic and infinitely extensible.
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# Reconcile your manifest into a running backend
|
|
26
|
-
$ telo ./examples/hello-api
|
|
26
|
+
$ telo ./examples/hello-api
|
|
27
27
|
|
|
28
28
|
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
|
|
29
29
|
```
|
|
@@ -44,146 +44,7 @@ $ telo ./examples/hello-api.yaml
|
|
|
44
44
|
|
|
45
45
|
## Example manifest
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```yaml
|
|
50
|
-
kind: Telo.Application
|
|
51
|
-
metadata:
|
|
52
|
-
name: feedback
|
|
53
|
-
version: 1.0.0
|
|
54
|
-
description: |
|
|
55
|
-
A complete feedback collection REST API — no code, pure YAML.
|
|
56
|
-
Persists entries to SQLite and serves them over HTTP.
|
|
57
|
-
imports:
|
|
58
|
-
Http: std/http-server@0.11.0
|
|
59
|
-
Sql: std/sql@0.9.0
|
|
60
|
-
targets:
|
|
61
|
-
- !ref Migrations
|
|
62
|
-
- !ref Server
|
|
63
|
-
---
|
|
64
|
-
# SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
|
|
65
|
-
kind: Sql.Connection
|
|
66
|
-
metadata:
|
|
67
|
-
name: Db
|
|
68
|
-
driver: sqlite
|
|
69
|
-
file: ./tmp/feedback.db
|
|
70
|
-
---
|
|
71
|
-
# Migrations: applied automatically before the server starts
|
|
72
|
-
kind: Sql.Migrations
|
|
73
|
-
metadata:
|
|
74
|
-
name: Migrations
|
|
75
|
-
connection: !ref Db
|
|
76
|
-
---
|
|
77
|
-
kind: Sql.Migration
|
|
78
|
-
metadata:
|
|
79
|
-
name: Migration_20260413_182154_CreateFeedback
|
|
80
|
-
version: 20260413_182154_CreateFeedback
|
|
81
|
-
sql: |
|
|
82
|
-
CREATE TABLE IF NOT EXISTS feedback (
|
|
83
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
-
text TEXT NOT NULL,
|
|
85
|
-
source TEXT,
|
|
86
|
-
score INTEGER NOT NULL DEFAULT 0,
|
|
87
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
88
|
-
)
|
|
89
|
-
---
|
|
90
|
-
kind: Http.Server
|
|
91
|
-
metadata:
|
|
92
|
-
name: Server
|
|
93
|
-
baseUrl: http://localhost:8844
|
|
94
|
-
port: 8844
|
|
95
|
-
logger: true
|
|
96
|
-
openapi:
|
|
97
|
-
info:
|
|
98
|
-
title: Feedback API
|
|
99
|
-
version: 1.0.0
|
|
100
|
-
mounts:
|
|
101
|
-
- path: /v1
|
|
102
|
-
mount: !ref FeedbackRoutes
|
|
103
|
-
---
|
|
104
|
-
kind: Http.Api
|
|
105
|
-
metadata:
|
|
106
|
-
name: FeedbackRoutes
|
|
107
|
-
routes:
|
|
108
|
-
# POST /v1/feedback — insert a new entry, score derived from body length heuristic
|
|
109
|
-
- request:
|
|
110
|
-
path: /feedback
|
|
111
|
-
method: POST
|
|
112
|
-
schema:
|
|
113
|
-
body:
|
|
114
|
-
type: object
|
|
115
|
-
properties:
|
|
116
|
-
text:
|
|
117
|
-
type: string
|
|
118
|
-
minLength: 1
|
|
119
|
-
source:
|
|
120
|
-
type: string
|
|
121
|
-
required: [ text ]
|
|
122
|
-
handler:
|
|
123
|
-
kind: Sql.Exec
|
|
124
|
-
connection: !ref Db
|
|
125
|
-
inputs:
|
|
126
|
-
sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
|
|
127
|
-
bindings:
|
|
128
|
-
- "${{ request.body.text }}"
|
|
129
|
-
- "${{ request.body.source }}"
|
|
130
|
-
- "${{ size(request.body.text) }}"
|
|
131
|
-
response:
|
|
132
|
-
- status: 201
|
|
133
|
-
headers:
|
|
134
|
-
Content-Type: application/json
|
|
135
|
-
body:
|
|
136
|
-
ok: true
|
|
137
|
-
message: Feedback received
|
|
138
|
-
|
|
139
|
-
# GET /v1/feedback — list all entries, newest first
|
|
140
|
-
- request:
|
|
141
|
-
path: /feedback
|
|
142
|
-
method: GET
|
|
143
|
-
handler:
|
|
144
|
-
kind: Sql.Select
|
|
145
|
-
connection: !ref Db
|
|
146
|
-
from: feedback
|
|
147
|
-
columns: [ id, text, source, score, created_at ]
|
|
148
|
-
orderBy:
|
|
149
|
-
- { column: created_at, direction: desc }
|
|
150
|
-
response:
|
|
151
|
-
- status: 200
|
|
152
|
-
headers:
|
|
153
|
-
Content-Type: application/json
|
|
154
|
-
body: "${{ result.rows }}"
|
|
155
|
-
|
|
156
|
-
# GET /v1/feedback/{id} — fetch a single entry
|
|
157
|
-
- request:
|
|
158
|
-
path: /feedback/{id}
|
|
159
|
-
method: GET
|
|
160
|
-
schema:
|
|
161
|
-
params:
|
|
162
|
-
type: object
|
|
163
|
-
properties:
|
|
164
|
-
id:
|
|
165
|
-
type: integer
|
|
166
|
-
required: [ id ]
|
|
167
|
-
handler:
|
|
168
|
-
kind: Sql.Select
|
|
169
|
-
connection: !ref Db
|
|
170
|
-
from: feedback
|
|
171
|
-
columns: [ id, text, source, score, created_at ]
|
|
172
|
-
where:
|
|
173
|
-
- { column: id, op: "=", value: "${{ request.params.id }}" }
|
|
174
|
-
response:
|
|
175
|
-
- status: 200
|
|
176
|
-
when: "size(result.rows) > 0"
|
|
177
|
-
headers:
|
|
178
|
-
Content-Type: application/json
|
|
179
|
-
body: "${{ result.rows[0] }}"
|
|
180
|
-
- status: 404
|
|
181
|
-
headers:
|
|
182
|
-
Content-Type: application/json
|
|
183
|
-
body:
|
|
184
|
-
ok: false
|
|
185
|
-
message: Not found
|
|
186
|
-
```
|
|
47
|
+
See [examples/](./examples/) for a list of working applications.
|
|
187
48
|
|
|
188
49
|
## Status
|
|
189
50
|
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { SqlConnectionResource, createSqlConnection, type SqlDriver, type PlaceholderStyle, } from "./sql-connection-controller.js";
|
|
2
|
+
export { isSqlConnection, resolveSqlConnection } from "./sql-connection-ref.js";
|
|
3
|
+
export type { SqliteDb, SqliteStatement } from "./sqlite-driver-interface.js";
|
package/dist/index.js
ADDED
|
@@ -2,7 +2,7 @@ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
|
2
2
|
import type { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
3
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
4
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
|
-
interface
|
|
5
|
+
interface SqlCommandManifest {
|
|
6
6
|
metadata: {
|
|
7
7
|
name: string;
|
|
8
8
|
module: string;
|
|
@@ -14,12 +14,12 @@ interface SqlExecManifest {
|
|
|
14
14
|
bindings?: unknown[];
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
declare class
|
|
17
|
+
declare class SqlCommandResource implements ResourceInstance {
|
|
18
18
|
private readonly manifest;
|
|
19
19
|
private readonly ctx;
|
|
20
|
-
constructor(manifest:
|
|
20
|
+
constructor(manifest: SqlCommandManifest, ctx: ResourceContext);
|
|
21
21
|
invoke(input: unknown): Promise<SqlResult>;
|
|
22
22
|
}
|
|
23
23
|
export declare function register(): void;
|
|
24
|
-
export declare function create(resource:
|
|
24
|
+
export declare function create(resource: SqlCommandManifest, ctx: ResourceContext): Promise<SqlCommandResource>;
|
|
25
25
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
2
2
|
import { runSql } from "./sql-run.js";
|
|
3
|
-
class
|
|
3
|
+
class SqlCommandResource {
|
|
4
4
|
manifest;
|
|
5
5
|
ctx;
|
|
6
6
|
constructor(manifest, ctx) {
|
|
@@ -10,7 +10,8 @@ class SqlExecResource {
|
|
|
10
10
|
async invoke(input) {
|
|
11
11
|
const m = this.manifest;
|
|
12
12
|
const ctx = this.ctx;
|
|
13
|
-
const connection = resolveSqlConnection(m.connection, ctx)
|
|
13
|
+
const connection = resolveSqlConnection(m.connection, ctx, () => `Sql.Command "${m.metadata.name}": 'connection'`) ??
|
|
14
|
+
m.transaction?.getConnection();
|
|
14
15
|
if (!connection) {
|
|
15
16
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
16
17
|
}
|
|
@@ -20,5 +21,5 @@ class SqlExecResource {
|
|
|
20
21
|
}
|
|
21
22
|
export function register() { }
|
|
22
23
|
export async function create(resource, ctx) {
|
|
23
|
-
return new
|
|
24
|
+
return new SqlCommandResource(resource, ctx);
|
|
24
25
|
}
|
|
@@ -2,27 +2,22 @@ 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";
|
|
5
|
-
interface PoolConfig {
|
|
6
|
-
min?: number;
|
|
7
|
-
max?: number;
|
|
8
|
-
idleTimeoutMs?: number;
|
|
9
|
-
connectionTimeoutMs?: number;
|
|
10
|
-
}
|
|
11
5
|
export type SqlDriver = "postgres" | "sqlite";
|
|
12
6
|
/** Native bind-placeholder syntax per driver: SQLite binds anonymous `?`,
|
|
13
7
|
* PostgreSQL binds numbered `$1`, `$2`, … */
|
|
14
8
|
export type PlaceholderStyle = "qmark" | "numbered";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Driver-agnostic SQL connection. The kysely instance (and, for SQLite, the
|
|
11
|
+
* underlying database handle used by `executeScript`) is built by the driver
|
|
12
|
+
* backend (`sql-postgres`, `sql-sqlite`) and handed in via
|
|
13
|
+
* {@link createSqlConnection}. Everything here — execution, transactions,
|
|
14
|
+
* placeholder style, row-count normalization — is transport-neutral.
|
|
15
|
+
*/
|
|
21
16
|
export declare class SqlConnectionResource implements ResourceInstance {
|
|
22
17
|
readonly driver: SqlDriver;
|
|
23
18
|
private readonly db;
|
|
24
19
|
private readonly sqlite?;
|
|
25
|
-
constructor(
|
|
20
|
+
constructor(driver: SqlDriver, db: Kysely<any>, sqlite?: SqliteDb);
|
|
26
21
|
init(): Promise<void>;
|
|
27
22
|
teardown(): Promise<void>;
|
|
28
23
|
transaction<T>(cb: () => Promise<T>): Promise<T>;
|
|
@@ -39,5 +34,10 @@ export declare class SqlConnectionResource implements ResourceInstance {
|
|
|
39
34
|
snapshot(): Record<string, unknown>;
|
|
40
35
|
private resolveExecutor;
|
|
41
36
|
}
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Build a connection from a driver-constructed kysely instance. Driver backends
|
|
39
|
+
* (`sql-postgres`, `sql-sqlite`) own dialect construction and call this; the
|
|
40
|
+
* `sqlite` handle is required only for SQLite (its `executeScript` runs through
|
|
41
|
+
* the native handle).
|
|
42
|
+
*/
|
|
43
|
+
export declare function createSqlConnection(driver: SqlDriver, db: Kysely<any>, sqlite?: SqliteDb): SqlConnectionResource;
|
|
@@ -1,44 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import { CompiledQuery
|
|
3
|
-
import { Pool } from "pg";
|
|
2
|
+
import { CompiledQuery } from "kysely";
|
|
4
3
|
import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Driver-agnostic SQL connection. The kysely instance (and, for SQLite, the
|
|
6
|
+
* underlying database handle used by `executeScript`) is built by the driver
|
|
7
|
+
* backend (`sql-postgres`, `sql-sqlite`) and handed in via
|
|
8
|
+
* {@link createSqlConnection}. Everything here — execution, transactions,
|
|
9
|
+
* placeholder style, row-count normalization — is transport-neutral.
|
|
10
|
+
*/
|
|
5
11
|
export class SqlConnectionResource {
|
|
6
12
|
driver;
|
|
7
13
|
db;
|
|
8
14
|
sqlite;
|
|
9
|
-
constructor(
|
|
10
|
-
this.driver =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
throw new Error("Sql: postgres connection requires a connectionString");
|
|
14
|
-
}
|
|
15
|
-
const url = new URL(config.connectionString);
|
|
16
|
-
const ssl = sslFromSslmode(url.searchParams.get("sslmode"));
|
|
17
|
-
url.searchParams.delete("sslmode");
|
|
18
|
-
this.db = new Kysely({
|
|
19
|
-
dialect: new PostgresDialect({
|
|
20
|
-
pool: new Pool({
|
|
21
|
-
connectionString: url.toString(),
|
|
22
|
-
ssl,
|
|
23
|
-
min: config.pool?.min ?? 1,
|
|
24
|
-
max: config.pool?.max ?? 10,
|
|
25
|
-
idleTimeoutMillis: config.pool?.idleTimeoutMs,
|
|
26
|
-
connectionTimeoutMillis: config.pool?.connectionTimeoutMs,
|
|
27
|
-
}),
|
|
28
|
-
}),
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
if (!sqlite) {
|
|
33
|
-
throw new Error("Sql: sqlite database was not initialized");
|
|
34
|
-
}
|
|
35
|
-
this.sqlite = sqlite;
|
|
36
|
-
this.db = new Kysely({
|
|
37
|
-
dialect: new TransactionalSqliteDialect({
|
|
38
|
-
database: this.sqlite,
|
|
39
|
-
}),
|
|
40
|
-
});
|
|
41
|
-
}
|
|
15
|
+
constructor(driver, db, sqlite) {
|
|
16
|
+
this.driver = driver;
|
|
17
|
+
this.db = db;
|
|
18
|
+
this.sqlite = sqlite;
|
|
42
19
|
}
|
|
43
20
|
async init() {
|
|
44
21
|
await this.db.connection().execute(async () => {
|
|
@@ -113,57 +90,12 @@ export class SqlConnectionResource {
|
|
|
113
90
|
return this.db;
|
|
114
91
|
}
|
|
115
92
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
class TransactionalSqliteDialect extends SqliteDialect {
|
|
126
|
-
createAdapter() {
|
|
127
|
-
return new TransactionalSqliteAdapter();
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function sslFromSslmode(mode) {
|
|
131
|
-
switch (mode) {
|
|
132
|
-
case null:
|
|
133
|
-
case "disable":
|
|
134
|
-
return false;
|
|
135
|
-
case "require":
|
|
136
|
-
return { rejectUnauthorized: false };
|
|
137
|
-
case "verify-ca":
|
|
138
|
-
// libpq `verify-ca` validates the CA chain but not the hostname; Node's
|
|
139
|
-
// default `checkServerIdentity` enforces the hostname, so disable it.
|
|
140
|
-
return { rejectUnauthorized: true, checkServerIdentity: () => undefined };
|
|
141
|
-
case "verify-full":
|
|
142
|
-
return { rejectUnauthorized: true };
|
|
143
|
-
default:
|
|
144
|
-
throw new Error(`Sql.Connection: unsupported sslmode '${mode}'. ` +
|
|
145
|
-
`Use 'disable', 'require', 'verify-ca', or 'verify-full'.`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
export async function openSqliteDatabase(file = ":memory:") {
|
|
149
|
-
// Auto-create the parent directory for file-backed databases. SQLite
|
|
150
|
-
// drivers fail-fast when the directory doesn't exist; mirroring `mkdir
|
|
151
|
-
// -p` here lets manifests use paths like `./tmp/foo.sqlite` without a
|
|
152
|
-
// separate filesystem-prep step. `:memory:` skips filesystem entirely.
|
|
153
|
-
if (file !== ":memory:") {
|
|
154
|
-
const { mkdir } = await import("node:fs/promises");
|
|
155
|
-
const { dirname } = await import("node:path");
|
|
156
|
-
const dir = dirname(file);
|
|
157
|
-
if (dir && dir !== "." && dir !== "/") {
|
|
158
|
-
await mkdir(dir, { recursive: true });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
// Route through the package's own `./sqlite-driver` subpath export so the
|
|
162
|
-
// resolver selects the driver per runtime (Bun → bun:sqlite, Node →
|
|
163
|
-
// better-sqlite3). A manual `typeof Bun` check with relative imports gets
|
|
164
|
-
// flattened by the controller bundler into an unconditional top-level
|
|
165
|
-
// `import "bun:sqlite"`, which Node's ESM loader rejects before the guard
|
|
166
|
-
// runs; an external `@telorun/*` specifier stays a deferred dynamic import.
|
|
167
|
-
const { openDatabase } = await import("@telorun/sql/sqlite-driver");
|
|
168
|
-
return openDatabase(file);
|
|
93
|
+
/**
|
|
94
|
+
* Build a connection from a driver-constructed kysely instance. Driver backends
|
|
95
|
+
* (`sql-postgres`, `sql-sqlite`) own dialect construction and call this; the
|
|
96
|
+
* `sqlite` handle is required only for SQLite (its `executeScript` runs through
|
|
97
|
+
* the native handle).
|
|
98
|
+
*/
|
|
99
|
+
export function createSqlConnection(driver, db, sqlite) {
|
|
100
|
+
return new SqlConnectionResource(driver, db, sqlite);
|
|
169
101
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { ResourceContext } from "@telorun/sdk";
|
|
1
|
+
import type { KindRef, ResourceContext } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/** True when a value already exposes the connection contract (Phase-5 injected). */
|
|
4
|
+
export declare function isSqlConnection(value: unknown): value is SqlConnectionResource;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a `connection` `!ref` field to a live connection. The slot is optional
|
|
7
|
+
* — an unset one yields `undefined` so the caller can fall back to a `transaction`
|
|
8
|
+
* — but a slot that IS set must resolve. `describe` names the owning resource and
|
|
9
|
+
* slot, so the failure points at a concrete manifest location.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveSqlConnection(value: SqlConnectionResource | KindRef<SqlConnectionResource> | undefined, ctx: ResourceContext, describe: () => string): SqlConnectionResource | undefined;
|
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** True when a value already exposes the connection contract (Phase-5 injected). */
|
|
2
|
+
export function isSqlConnection(value) {
|
|
3
|
+
return typeof value?.execute === "function";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a `connection` `!ref` field to a live connection. The slot is optional
|
|
7
|
+
* — an unset one yields `undefined` so the caller can fall back to a `transaction`
|
|
8
|
+
* — but a slot that IS set must resolve. `describe` names the owning resource and
|
|
9
|
+
* slot, so the failure points at a concrete manifest location.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveSqlConnection(value, ctx, describe) {
|
|
12
|
+
if (!value)
|
|
3
13
|
return undefined;
|
|
4
|
-
|
|
5
|
-
if (typeof value.execute === "function") {
|
|
6
|
-
return value;
|
|
7
|
-
}
|
|
8
|
-
const ref = value;
|
|
9
|
-
if (typeof ref.name !== "string") {
|
|
10
|
-
throw new Error("Sql: invalid connection reference");
|
|
11
|
-
}
|
|
12
|
-
// Cross-module reference (`!ref Alias.Connection`): a connection resolved
|
|
13
|
-
// inside a nested library is not Phase-5-injected, so the controller receives
|
|
14
|
-
// the raw `{name, alias}` ref and must route through the import's exported
|
|
15
|
-
// scope rather than a bare local lookup.
|
|
16
|
-
if (ref.alias && ref.alias !== "Self") {
|
|
17
|
-
const instance = ctx.moduleContext.resolveImportedInstance(ref.alias, ref.name);
|
|
18
|
-
if (typeof instance?.execute !== "function") {
|
|
19
|
-
throw new Error(`Sql: connection reference '${ref.alias}.${ref.name}' did not resolve to an exported connection instance.`);
|
|
20
|
-
}
|
|
21
|
-
return instance;
|
|
22
|
-
}
|
|
23
|
-
return ctx.moduleContext.getInstance(ref.name);
|
|
14
|
+
return ctx.resolveRef(value, isSqlConnection, describe, "std/sql#Connection");
|
|
24
15
|
}
|
|
@@ -32,7 +32,7 @@ class SqlMigrationsResource {
|
|
|
32
32
|
this.ctx = ctx;
|
|
33
33
|
}
|
|
34
34
|
async run() {
|
|
35
|
-
const conn = resolveSqlConnection(this.manifest.connection, this.ctx) ?? failMissingConnection();
|
|
35
|
+
const conn = resolveSqlConnection(this.manifest.connection, this.ctx, () => `Sql.Migrations "${this.manifest.metadata.name}": 'connection'`) ?? failMissingConnection();
|
|
36
36
|
const migrations = {};
|
|
37
37
|
// Legacy: standalone `Sql.Migration` resources in the same module scope.
|
|
38
38
|
for (const [, { resource }] of this.ctx.moduleContext.resourceInstances) {
|
|
@@ -10,13 +10,15 @@ class SqlQueryResource {
|
|
|
10
10
|
async invoke(input) {
|
|
11
11
|
const m = this.manifest;
|
|
12
12
|
const ctx = this.ctx;
|
|
13
|
-
const connection = resolveConnection(m.connection, m.transaction, ctx);
|
|
13
|
+
const connection = resolveConnection(m.connection, m.transaction, ctx, () => `Sql.Query "${m.metadata.name}": 'connection'`);
|
|
14
14
|
const result = await runSql(connection, m.transaction, input, ctx);
|
|
15
15
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
function resolveConnection(connection, transaction, ctx) {
|
|
19
|
-
return (resolveSqlConnection(connection, ctx
|
|
18
|
+
function resolveConnection(connection, transaction, ctx, describe) {
|
|
19
|
+
return (resolveSqlConnection(connection, ctx, describe) ??
|
|
20
|
+
transaction?.getConnection() ??
|
|
21
|
+
failMissingConnection());
|
|
20
22
|
}
|
|
21
23
|
function failMissingConnection() {
|
|
22
24
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
@@ -58,12 +58,12 @@ interface SelectManifest {
|
|
|
58
58
|
offset?: unknown;
|
|
59
59
|
inputType?: string | Record<string, any>;
|
|
60
60
|
}
|
|
61
|
-
declare class
|
|
61
|
+
declare class SqlSelectionResource implements ResourceInstance {
|
|
62
62
|
private readonly manifest;
|
|
63
63
|
private readonly ctx;
|
|
64
64
|
constructor(manifest: SelectManifest, ctx: ResourceContext);
|
|
65
65
|
invoke(input: unknown): Promise<SqlResult>;
|
|
66
66
|
}
|
|
67
67
|
export declare function register(): void;
|
|
68
|
-
export declare function create(resource: SelectManifest, ctx: ResourceContext): Promise<
|
|
68
|
+
export declare function create(resource: SelectManifest, ctx: ResourceContext): Promise<SqlSelectionResource>;
|
|
69
69
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
2
2
|
// ── Controller ────────────────────────────────────────────────────────────────
|
|
3
|
-
class
|
|
3
|
+
class SqlSelectionResource {
|
|
4
4
|
manifest;
|
|
5
5
|
ctx;
|
|
6
6
|
constructor(manifest, ctx) {
|
|
@@ -19,9 +19,10 @@ class SqlSelectResource {
|
|
|
19
19
|
const having = ctx.expandValue(m.having ?? [], expandCtx);
|
|
20
20
|
const limit = m.limit != null ? ctx.expandValue(m.limit, expandCtx) : undefined;
|
|
21
21
|
const offset = m.offset != null ? ctx.expandValue(m.offset, expandCtx) : undefined;
|
|
22
|
-
const connection = resolveSqlConnection(m.connection, ctx)
|
|
22
|
+
const connection = resolveSqlConnection(m.connection, ctx, () => `Sql.Selection "${m.metadata.name}": 'connection'`) ??
|
|
23
|
+
m.transaction?.getConnection();
|
|
23
24
|
if (!connection) {
|
|
24
|
-
throw new Error("Sql.
|
|
25
|
+
throw new Error("Sql.Selection: either 'connection' or 'transaction' must be set");
|
|
25
26
|
}
|
|
26
27
|
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.driver);
|
|
27
28
|
const result = await connection.execute(sql, params, m.transaction);
|
|
@@ -157,7 +158,7 @@ function opToSql(op) {
|
|
|
157
158
|
};
|
|
158
159
|
const sql = map[op];
|
|
159
160
|
if (!sql)
|
|
160
|
-
throw new Error(`Sql.
|
|
161
|
+
throw new Error(`Sql.Selection: unknown operator '${op}'`);
|
|
161
162
|
return sql;
|
|
162
163
|
}
|
|
163
164
|
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
@@ -190,5 +191,5 @@ function extractDefaults(inputType, ctx) {
|
|
|
190
191
|
}
|
|
191
192
|
export function register() { }
|
|
192
193
|
export async function create(resource, ctx) {
|
|
193
|
-
return new
|
|
194
|
+
return new SqlSelectionResource(resource, ctx);
|
|
194
195
|
}
|
|
@@ -8,8 +8,7 @@ export class SqlTransactionResource {
|
|
|
8
8
|
this.ctx = ctx;
|
|
9
9
|
}
|
|
10
10
|
getConnection() {
|
|
11
|
-
return (resolveSqlConnection(this.manifest.connection, this.ctx) ??
|
|
12
|
-
failMissingConnection(this.manifest.metadata.name));
|
|
11
|
+
return (resolveSqlConnection(this.manifest.connection, this.ctx, () => `Sql.Transaction "${this.manifest.metadata.name}": 'connection'`) ?? failMissingConnection(this.manifest.metadata.name));
|
|
13
12
|
}
|
|
14
13
|
assertActive() {
|
|
15
14
|
if (!currentTxId()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -21,35 +21,36 @@
|
|
|
21
21
|
"url": "https://github.com/telorun/telo/issues"
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
24
27
|
"exports": {
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"bun": "./src/index.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
28
32
|
},
|
|
29
|
-
"./
|
|
30
|
-
"
|
|
31
|
-
"
|
|
33
|
+
"./connection": {
|
|
34
|
+
"types": "./dist/sql-connection-controller.d.ts",
|
|
35
|
+
"bun": "./src/sql-connection-controller.ts",
|
|
36
|
+
"import": "./dist/sql-connection-controller.js"
|
|
32
37
|
},
|
|
33
38
|
"./sql-query": {
|
|
34
39
|
"bun": "./src/sql-query-controller.ts",
|
|
35
40
|
"import": "./dist/sql-query-controller.js"
|
|
36
41
|
},
|
|
37
|
-
"./sql-
|
|
38
|
-
"bun": "./src/sql-
|
|
39
|
-
"import": "./dist/sql-
|
|
42
|
+
"./sql-selection": {
|
|
43
|
+
"bun": "./src/sql-selection-controller.ts",
|
|
44
|
+
"import": "./dist/sql-selection-controller.js"
|
|
40
45
|
},
|
|
41
|
-
"./sql-
|
|
42
|
-
"bun": "./src/sql-
|
|
43
|
-
"import": "./dist/sql-
|
|
46
|
+
"./sql-command": {
|
|
47
|
+
"bun": "./src/sql-command-controller.ts",
|
|
48
|
+
"import": "./dist/sql-command-controller.js"
|
|
44
49
|
},
|
|
45
50
|
"./sql-transaction": {
|
|
46
51
|
"bun": "./src/sql-transaction-controller.ts",
|
|
47
52
|
"import": "./dist/sql-transaction-controller.js"
|
|
48
53
|
},
|
|
49
|
-
"./sqlite-driver": {
|
|
50
|
-
"bun": "./src/sqlite-driver-bun.ts",
|
|
51
|
-
"import": "./dist/sqlite-driver-node.js"
|
|
52
|
-
},
|
|
53
54
|
"./sql-migration": {
|
|
54
55
|
"bun": "./src/sql-migration-controller.ts",
|
|
55
56
|
"import": "./dist/sql-migration-controller.js"
|
|
@@ -64,17 +65,12 @@
|
|
|
64
65
|
"src/**"
|
|
65
66
|
],
|
|
66
67
|
"dependencies": {
|
|
67
|
-
"better-sqlite3": "^12.8.0",
|
|
68
|
-
"pg": "^8.20.0",
|
|
69
68
|
"kysely": "^0.28.15"
|
|
70
69
|
},
|
|
71
70
|
"devDependencies": {
|
|
72
|
-
"@types/better-sqlite3": "^7.0.0",
|
|
73
|
-
"@types/bun": "^1.3.10",
|
|
74
71
|
"@types/node": "^20.0.0",
|
|
75
|
-
"@types/pg": "^8.0.0",
|
|
76
72
|
"typescript": "^5.0.0",
|
|
77
|
-
"@telorun/sdk": "0.
|
|
73
|
+
"@telorun/sdk": "0.54.0"
|
|
78
74
|
},
|
|
79
75
|
"peerDependencies": {
|
|
80
76
|
"@telorun/sdk": "*"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SqlConnectionResource,
|
|
3
|
+
createSqlConnection,
|
|
4
|
+
type SqlDriver,
|
|
5
|
+
type PlaceholderStyle,
|
|
6
|
+
} from "./sql-connection-controller.js";
|
|
7
|
+
export { isSqlConnection, resolveSqlConnection } from "./sql-connection-ref.js";
|
|
8
|
+
export type { SqliteDb, SqliteStatement } from "./sqlite-driver-interface.js";
|
|
@@ -5,7 +5,7 @@ import type { SqlResult } from "./sql-query-controller.js";
|
|
|
5
5
|
import { runSql } from "./sql-run.js";
|
|
6
6
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
7
7
|
|
|
8
|
-
interface
|
|
8
|
+
interface SqlCommandManifest {
|
|
9
9
|
metadata: { name: string; module: string };
|
|
10
10
|
connection?: SqlConnectionResource;
|
|
11
11
|
transaction?: SqlTransactionResource;
|
|
@@ -15,9 +15,9 @@ interface SqlExecManifest {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
class
|
|
18
|
+
class SqlCommandResource implements ResourceInstance {
|
|
19
19
|
constructor(
|
|
20
|
-
private readonly manifest:
|
|
20
|
+
private readonly manifest: SqlCommandManifest,
|
|
21
21
|
private readonly ctx: ResourceContext,
|
|
22
22
|
) {}
|
|
23
23
|
|
|
@@ -25,7 +25,9 @@ class SqlExecResource implements ResourceInstance {
|
|
|
25
25
|
const m = this.manifest;
|
|
26
26
|
const ctx = this.ctx;
|
|
27
27
|
|
|
28
|
-
const connection =
|
|
28
|
+
const connection =
|
|
29
|
+
resolveSqlConnection(m.connection, ctx, () => `Sql.Command "${m.metadata.name}": 'connection'`) ??
|
|
30
|
+
m.transaction?.getConnection();
|
|
29
31
|
if (!connection) {
|
|
30
32
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
31
33
|
}
|
|
@@ -38,8 +40,8 @@ class SqlExecResource implements ResourceInstance {
|
|
|
38
40
|
export function register(): void {}
|
|
39
41
|
|
|
40
42
|
export async function create(
|
|
41
|
-
resource:
|
|
43
|
+
resource: SqlCommandManifest,
|
|
42
44
|
ctx: ResourceContext,
|
|
43
|
-
): Promise<
|
|
44
|
-
return new
|
|
45
|
+
): Promise<SqlCommandResource> {
|
|
46
|
+
return new SqlCommandResource(resource, ctx);
|
|
45
47
|
}
|
|
@@ -1,77 +1,34 @@
|
|
|
1
1
|
import type { ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
|
-
import {
|
|
4
|
-
CompiledQuery,
|
|
5
|
-
Kysely,
|
|
6
|
-
PostgresDialect,
|
|
7
|
-
SqliteAdapter,
|
|
8
|
-
SqliteDialect,
|
|
9
|
-
type QueryResult,
|
|
10
|
-
type Transaction,
|
|
11
|
-
} from "kysely";
|
|
12
|
-
import { Pool } from "pg";
|
|
3
|
+
import { CompiledQuery, Kysely, type QueryResult, type Transaction } from "kysely";
|
|
13
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
14
5
|
import type { SqliteDb } from "./sqlite-driver-interface.js";
|
|
15
6
|
import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-store.js";
|
|
16
7
|
|
|
17
|
-
interface PoolConfig {
|
|
18
|
-
min?: number;
|
|
19
|
-
max?: number;
|
|
20
|
-
idleTimeoutMs?: number;
|
|
21
|
-
connectionTimeoutMs?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
8
|
export type SqlDriver = "postgres" | "sqlite";
|
|
25
9
|
|
|
26
10
|
/** Native bind-placeholder syntax per driver: SQLite binds anonymous `?`,
|
|
27
11
|
* PostgreSQL binds numbered `$1`, `$2`, … */
|
|
28
12
|
export type PlaceholderStyle = "qmark" | "numbered";
|
|
29
13
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Driver-agnostic SQL connection. The kysely instance (and, for SQLite, the
|
|
16
|
+
* underlying database handle used by `executeScript`) is built by the driver
|
|
17
|
+
* backend (`sql-postgres`, `sql-sqlite`) and handed in via
|
|
18
|
+
* {@link createSqlConnection}. Everything here — execution, transactions,
|
|
19
|
+
* placeholder style, row-count normalization — is transport-neutral.
|
|
20
|
+
*/
|
|
37
21
|
export class SqlConnectionResource implements ResourceInstance {
|
|
38
|
-
readonly driver: SqlDriver;
|
|
39
22
|
private readonly db: Kysely<any>;
|
|
40
23
|
private readonly sqlite?: SqliteDb;
|
|
41
24
|
|
|
42
|
-
constructor(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const url = new URL(config.connectionString);
|
|
50
|
-
const ssl = sslFromSslmode(url.searchParams.get("sslmode"));
|
|
51
|
-
url.searchParams.delete("sslmode");
|
|
52
|
-
this.db = new Kysely({
|
|
53
|
-
dialect: new PostgresDialect({
|
|
54
|
-
pool: new Pool({
|
|
55
|
-
connectionString: url.toString(),
|
|
56
|
-
ssl,
|
|
57
|
-
min: config.pool?.min ?? 1,
|
|
58
|
-
max: config.pool?.max ?? 10,
|
|
59
|
-
idleTimeoutMillis: config.pool?.idleTimeoutMs,
|
|
60
|
-
connectionTimeoutMillis: config.pool?.connectionTimeoutMs,
|
|
61
|
-
}),
|
|
62
|
-
}),
|
|
63
|
-
});
|
|
64
|
-
} else {
|
|
65
|
-
if (!sqlite) {
|
|
66
|
-
throw new Error("Sql: sqlite database was not initialized");
|
|
67
|
-
}
|
|
68
|
-
this.sqlite = sqlite;
|
|
69
|
-
this.db = new Kysely({
|
|
70
|
-
dialect: new TransactionalSqliteDialect({
|
|
71
|
-
database: this.sqlite,
|
|
72
|
-
}),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
25
|
+
constructor(
|
|
26
|
+
readonly driver: SqlDriver,
|
|
27
|
+
db: Kysely<any>,
|
|
28
|
+
sqlite?: SqliteDb,
|
|
29
|
+
) {
|
|
30
|
+
this.db = db;
|
|
31
|
+
this.sqlite = sqlite;
|
|
75
32
|
}
|
|
76
33
|
|
|
77
34
|
async init() {
|
|
@@ -171,67 +128,16 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
171
128
|
}
|
|
172
129
|
}
|
|
173
130
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return new TransactionalSqliteAdapter();
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
type SslOption =
|
|
191
|
-
| false
|
|
192
|
-
| { rejectUnauthorized: boolean; checkServerIdentity?: () => undefined };
|
|
193
|
-
|
|
194
|
-
function sslFromSslmode(mode: string | null): SslOption {
|
|
195
|
-
switch (mode) {
|
|
196
|
-
case null:
|
|
197
|
-
case "disable":
|
|
198
|
-
return false;
|
|
199
|
-
case "require":
|
|
200
|
-
return { rejectUnauthorized: false };
|
|
201
|
-
case "verify-ca":
|
|
202
|
-
// libpq `verify-ca` validates the CA chain but not the hostname; Node's
|
|
203
|
-
// default `checkServerIdentity` enforces the hostname, so disable it.
|
|
204
|
-
return { rejectUnauthorized: true, checkServerIdentity: () => undefined };
|
|
205
|
-
case "verify-full":
|
|
206
|
-
return { rejectUnauthorized: true };
|
|
207
|
-
default:
|
|
208
|
-
throw new Error(
|
|
209
|
-
`Sql.Connection: unsupported sslmode '${mode}'. ` +
|
|
210
|
-
`Use 'disable', 'require', 'verify-ca', or 'verify-full'.`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export async function openSqliteDatabase(file = ":memory:"): Promise<SqliteDb> {
|
|
216
|
-
// Auto-create the parent directory for file-backed databases. SQLite
|
|
217
|
-
// drivers fail-fast when the directory doesn't exist; mirroring `mkdir
|
|
218
|
-
// -p` here lets manifests use paths like `./tmp/foo.sqlite` without a
|
|
219
|
-
// separate filesystem-prep step. `:memory:` skips filesystem entirely.
|
|
220
|
-
if (file !== ":memory:") {
|
|
221
|
-
const { mkdir } = await import("node:fs/promises");
|
|
222
|
-
const { dirname } = await import("node:path");
|
|
223
|
-
const dir = dirname(file);
|
|
224
|
-
if (dir && dir !== "." && dir !== "/") {
|
|
225
|
-
await mkdir(dir, { recursive: true });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Route through the package's own `./sqlite-driver` subpath export so the
|
|
230
|
-
// resolver selects the driver per runtime (Bun → bun:sqlite, Node →
|
|
231
|
-
// better-sqlite3). A manual `typeof Bun` check with relative imports gets
|
|
232
|
-
// flattened by the controller bundler into an unconditional top-level
|
|
233
|
-
// `import "bun:sqlite"`, which Node's ESM loader rejects before the guard
|
|
234
|
-
// runs; an external `@telorun/*` specifier stays a deferred dynamic import.
|
|
235
|
-
const { openDatabase } = await import("@telorun/sql/sqlite-driver");
|
|
236
|
-
return openDatabase(file);
|
|
131
|
+
/**
|
|
132
|
+
* Build a connection from a driver-constructed kysely instance. Driver backends
|
|
133
|
+
* (`sql-postgres`, `sql-sqlite`) own dialect construction and call this; the
|
|
134
|
+
* `sqlite` handle is required only for SQLite (its `executeScript` runs through
|
|
135
|
+
* the native handle).
|
|
136
|
+
*/
|
|
137
|
+
export function createSqlConnection(
|
|
138
|
+
driver: SqlDriver,
|
|
139
|
+
db: Kysely<any>,
|
|
140
|
+
sqlite?: SqliteDb,
|
|
141
|
+
): SqlConnectionResource {
|
|
142
|
+
return new SqlConnectionResource(driver, db, sqlite);
|
|
237
143
|
}
|
|
@@ -1,41 +1,22 @@
|
|
|
1
|
-
import type { ResourceContext } from "@telorun/sdk";
|
|
1
|
+
import type { KindRef, ResourceContext } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnectionResource } from "./sql-connection-controller.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/** True when a value already exposes the connection contract (Phase-5 injected). */
|
|
5
|
+
export function isSqlConnection(value: unknown): value is SqlConnectionResource {
|
|
6
|
+
return typeof (value as SqlConnectionResource | undefined)?.execute === "function";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a `connection` `!ref` field to a live connection. The slot is optional
|
|
11
|
+
* — an unset one yields `undefined` so the caller can fall back to a `transaction`
|
|
12
|
+
* — but a slot that IS set must resolve. `describe` names the owning resource and
|
|
13
|
+
* slot, so the failure points at a concrete manifest location.
|
|
14
|
+
*/
|
|
9
15
|
export function resolveSqlConnection(
|
|
10
|
-
value: SqlConnectionResource |
|
|
16
|
+
value: SqlConnectionResource | KindRef<SqlConnectionResource> | undefined,
|
|
11
17
|
ctx: ResourceContext,
|
|
18
|
+
describe: () => string,
|
|
12
19
|
): SqlConnectionResource | undefined {
|
|
13
|
-
if (!value)
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (typeof (value as SqlConnectionResource).execute === "function") {
|
|
18
|
-
return value as SqlConnectionResource;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const ref = value as ConnectionRef;
|
|
22
|
-
if (typeof ref.name !== "string") {
|
|
23
|
-
throw new Error("Sql: invalid connection reference");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Cross-module reference (`!ref Alias.Connection`): a connection resolved
|
|
27
|
-
// inside a nested library is not Phase-5-injected, so the controller receives
|
|
28
|
-
// the raw `{name, alias}` ref and must route through the import's exported
|
|
29
|
-
// scope rather than a bare local lookup.
|
|
30
|
-
if (ref.alias && ref.alias !== "Self") {
|
|
31
|
-
const instance = ctx.moduleContext.resolveImportedInstance(ref.alias, ref.name);
|
|
32
|
-
if (typeof (instance as SqlConnectionResource | undefined)?.execute !== "function") {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`Sql: connection reference '${ref.alias}.${ref.name}' did not resolve to an exported connection instance.`,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return instance as unknown as SqlConnectionResource;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return ctx.moduleContext.getInstance(ref.name) as SqlConnectionResource;
|
|
20
|
+
if (!value) return undefined;
|
|
21
|
+
return ctx.resolveRef(value, isSqlConnection, describe, "std/sql#Connection");
|
|
41
22
|
}
|
|
@@ -56,7 +56,11 @@ class SqlMigrationsResource implements ResourceInstance {
|
|
|
56
56
|
|
|
57
57
|
async run(): Promise<void> {
|
|
58
58
|
const conn =
|
|
59
|
-
resolveSqlConnection(
|
|
59
|
+
resolveSqlConnection(
|
|
60
|
+
this.manifest.connection,
|
|
61
|
+
this.ctx,
|
|
62
|
+
() => `Sql.Migrations "${this.manifest.metadata.name}": 'connection'`,
|
|
63
|
+
) ?? failMissingConnection();
|
|
60
64
|
|
|
61
65
|
const migrations: Record<string, string[]> = {};
|
|
62
66
|
// Legacy: standalone `Sql.Migration` resources in the same module scope.
|
|
@@ -28,7 +28,12 @@ class SqlQueryResource implements ResourceInstance {
|
|
|
28
28
|
async invoke(input: unknown): Promise<SqlResult> {
|
|
29
29
|
const m = this.manifest;
|
|
30
30
|
const ctx = this.ctx;
|
|
31
|
-
const connection = resolveConnection(
|
|
31
|
+
const connection = resolveConnection(
|
|
32
|
+
m.connection,
|
|
33
|
+
m.transaction,
|
|
34
|
+
ctx,
|
|
35
|
+
() => `Sql.Query "${m.metadata.name}": 'connection'`,
|
|
36
|
+
);
|
|
32
37
|
const result = await runSql(connection, m.transaction, input, ctx);
|
|
33
38
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
34
39
|
}
|
|
@@ -38,9 +43,12 @@ function resolveConnection(
|
|
|
38
43
|
connection: SqlConnectionResource | undefined,
|
|
39
44
|
transaction: SqlTransactionResource | undefined,
|
|
40
45
|
ctx: ResourceContext,
|
|
46
|
+
describe: () => string,
|
|
41
47
|
): SqlConnectionResource {
|
|
42
48
|
return (
|
|
43
|
-
resolveSqlConnection(connection, ctx
|
|
49
|
+
resolveSqlConnection(connection, ctx, describe) ??
|
|
50
|
+
transaction?.getConnection() ??
|
|
51
|
+
failMissingConnection()
|
|
44
52
|
);
|
|
45
53
|
}
|
|
46
54
|
|
|
@@ -76,7 +76,7 @@ interface SelectManifest {
|
|
|
76
76
|
|
|
77
77
|
// ── Controller ────────────────────────────────────────────────────────────────
|
|
78
78
|
|
|
79
|
-
class
|
|
79
|
+
class SqlSelectionResource implements ResourceInstance {
|
|
80
80
|
constructor(
|
|
81
81
|
private readonly manifest: SelectManifest,
|
|
82
82
|
private readonly ctx: ResourceContext,
|
|
@@ -96,9 +96,11 @@ class SqlSelectResource implements ResourceInstance {
|
|
|
96
96
|
const limit = m.limit != null ? ctx.expandValue(m.limit, expandCtx) : undefined;
|
|
97
97
|
const offset = m.offset != null ? ctx.expandValue(m.offset, expandCtx) : undefined;
|
|
98
98
|
|
|
99
|
-
const connection =
|
|
99
|
+
const connection =
|
|
100
|
+
resolveSqlConnection(m.connection, ctx, () => `Sql.Selection "${m.metadata.name}": 'connection'`) ??
|
|
101
|
+
m.transaction?.getConnection();
|
|
100
102
|
if (!connection) {
|
|
101
|
-
throw new Error("Sql.
|
|
103
|
+
throw new Error("Sql.Selection: either 'connection' or 'transaction' must be set");
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.driver);
|
|
@@ -266,7 +268,7 @@ function opToSql(op: Op): string {
|
|
|
266
268
|
ilike: "ILIKE",
|
|
267
269
|
};
|
|
268
270
|
const sql = map[op];
|
|
269
|
-
if (!sql) throw new Error(`Sql.
|
|
271
|
+
if (!sql) throw new Error(`Sql.Selection: unknown operator '${op}'`);
|
|
270
272
|
return sql;
|
|
271
273
|
}
|
|
272
274
|
|
|
@@ -306,6 +308,6 @@ export function register(): void {}
|
|
|
306
308
|
export async function create(
|
|
307
309
|
resource: SelectManifest,
|
|
308
310
|
ctx: ResourceContext,
|
|
309
|
-
): Promise<
|
|
310
|
-
return new
|
|
311
|
+
): Promise<SqlSelectionResource> {
|
|
312
|
+
return new SqlSelectionResource(resource, ctx);
|
|
311
313
|
}
|
|
@@ -18,8 +18,11 @@ export class SqlTransactionResource implements ResourceInstance {
|
|
|
18
18
|
|
|
19
19
|
getConnection(): SqlConnectionResource {
|
|
20
20
|
return (
|
|
21
|
-
resolveSqlConnection(
|
|
22
|
-
|
|
21
|
+
resolveSqlConnection(
|
|
22
|
+
this.manifest.connection,
|
|
23
|
+
this.ctx,
|
|
24
|
+
() => `Sql.Transaction "${this.manifest.metadata.name}": 'connection'`,
|
|
25
|
+
) ?? failMissingConnection(this.manifest.metadata.name)
|
|
23
26
|
);
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -1,19 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,9 +0,0 @@
|
|
|
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,13 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,6 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
|
-
export function openDatabase(file) {
|
|
3
|
-
const db = new Database(file);
|
|
4
|
-
return {
|
|
5
|
-
prepare(sql) {
|
|
6
|
-
const stmt = db.prepare(sql);
|
|
7
|
-
return {
|
|
8
|
-
reader: true,
|
|
9
|
-
all(params) {
|
|
10
|
-
return stmt.all(...params);
|
|
11
|
-
},
|
|
12
|
-
run(params) {
|
|
13
|
-
const result = stmt.run(...params);
|
|
14
|
-
return {
|
|
15
|
-
changes: result.changes,
|
|
16
|
-
lastInsertRowid: result.lastInsertRowid,
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
iterate(params) {
|
|
20
|
-
return stmt.iterate(...params);
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
},
|
|
24
|
-
exec(sql) {
|
|
25
|
-
db.exec(sql);
|
|
26
|
-
},
|
|
27
|
-
close() {
|
|
28
|
-
db.close();
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import Database from "better-sqlite3";
|
|
2
|
-
export function openDatabase(file) {
|
|
3
|
-
const db = new Database(file);
|
|
4
|
-
return {
|
|
5
|
-
prepare(sql) {
|
|
6
|
-
const stmt = db.prepare(sql);
|
|
7
|
-
return {
|
|
8
|
-
reader: stmt.reader,
|
|
9
|
-
all(params) {
|
|
10
|
-
return stmt.all(...params);
|
|
11
|
-
},
|
|
12
|
-
run(params) {
|
|
13
|
-
const result = stmt.run(...params);
|
|
14
|
-
return {
|
|
15
|
-
changes: result.changes,
|
|
16
|
-
lastInsertRowid: result.lastInsertRowid,
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
iterate(params) {
|
|
20
|
-
return stmt.iterate(...params);
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
},
|
|
24
|
-
exec(sql) {
|
|
25
|
-
db.exec(sql);
|
|
26
|
-
},
|
|
27
|
-
close() {
|
|
28
|
-
db.close();
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
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,18 +0,0 @@
|
|
|
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
|
-
}
|
package/src/sqlite-driver-bun.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
|
-
import type { SqliteDb } from "./sqlite-driver-interface.js";
|
|
3
|
-
|
|
4
|
-
export function openDatabase(file: string): SqliteDb {
|
|
5
|
-
const db = new Database(file);
|
|
6
|
-
|
|
7
|
-
return {
|
|
8
|
-
prepare(sql: string) {
|
|
9
|
-
const stmt = db.prepare(sql);
|
|
10
|
-
return {
|
|
11
|
-
reader: true,
|
|
12
|
-
all(params: ReadonlyArray<unknown>) {
|
|
13
|
-
return stmt.all(...(params as any[]));
|
|
14
|
-
},
|
|
15
|
-
run(params: ReadonlyArray<unknown>) {
|
|
16
|
-
const result = stmt.run(...(params as any[]));
|
|
17
|
-
return {
|
|
18
|
-
changes: result.changes,
|
|
19
|
-
lastInsertRowid: result.lastInsertRowid,
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
iterate(params: ReadonlyArray<unknown>) {
|
|
23
|
-
return stmt.iterate(...(params as any[])) as IterableIterator<unknown>;
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
exec(sql: string) {
|
|
28
|
-
db.exec(sql);
|
|
29
|
-
},
|
|
30
|
-
close() {
|
|
31
|
-
db.close();
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import Database from "better-sqlite3";
|
|
2
|
-
import type { SqliteDb } from "./sqlite-driver-interface.js";
|
|
3
|
-
|
|
4
|
-
export function openDatabase(file: string): SqliteDb {
|
|
5
|
-
const db = new Database(file);
|
|
6
|
-
|
|
7
|
-
return {
|
|
8
|
-
prepare(sql: string) {
|
|
9
|
-
const stmt = db.prepare(sql);
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
reader: stmt.reader,
|
|
13
|
-
all(params: ReadonlyArray<unknown>) {
|
|
14
|
-
return stmt.all(...(params as unknown[]));
|
|
15
|
-
},
|
|
16
|
-
run(params: ReadonlyArray<unknown>) {
|
|
17
|
-
const result = stmt.run(...(params as unknown[]));
|
|
18
|
-
return {
|
|
19
|
-
changes: result.changes,
|
|
20
|
-
lastInsertRowid: result.lastInsertRowid,
|
|
21
|
-
};
|
|
22
|
-
},
|
|
23
|
-
iterate(params: ReadonlyArray<unknown>) {
|
|
24
|
-
return stmt.iterate(...(params as unknown[])) as IterableIterator<unknown>;
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
},
|
|
28
|
-
exec(sql: string) {
|
|
29
|
-
db.exec(sql);
|
|
30
|
-
},
|
|
31
|
-
close() {
|
|
32
|
-
db.close();
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|