@telorun/sql 0.10.0 → 0.12.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 +3 -1
- package/dist/sql-command-controller.d.ts +2 -2
- package/dist/sql-command-controller.js +7 -2
- package/dist/sql-connection-base.d.ts +8 -5
- package/dist/sql-connection-base.js +31 -29
- package/dist/sql-connection.d.ts +16 -5
- package/dist/sql-query-controller.d.ts +2 -2
- package/dist/sql-query-controller.js +5 -2
- package/dist/sql-run.d.ts +2 -3
- package/dist/sql-run.js +4 -4
- package/dist/sql-selection-controller.d.ts +2 -2
- package/dist/sql-selection-controller.js +5 -2
- package/dist/sql-transaction-controller.d.ts +4 -4
- package/dist/sql-transaction-controller.js +45 -13
- package/package.json +5 -27
- package/src/sql-command-controller.ts +8 -3
- package/src/sql-connection-base.ts +42 -30
- package/src/sql-connection.ts +22 -5
- package/src/sql-query-controller.ts +6 -3
- package/src/sql-run.ts +13 -5
- package/src/sql-selection-controller.ts +6 -3
- package/src/sql-transaction-controller.ts +68 -19
- package/dist/transaction-store.d.ts +0 -9
- package/dist/transaction-store.js +0 -11
- package/src/transaction-store.ts +0 -20
package/README.md
CHANGED
|
@@ -48,7 +48,9 @@ See [examples/](./examples/) for a list of working applications.
|
|
|
48
48
|
|
|
49
49
|
## Status
|
|
50
50
|
|
|
51
|
-
Telo is under
|
|
51
|
+
Telo is under heavy development. While it is pre-1.0, breaking changes ship in **minor** releases — manifest shapes, kind schemas, and APIs can change between versions. Pin your imports and expect to update manifests when you upgrade. The core runtime, module system, and standard library are functional, but Telo is not yet recommended for production use.
|
|
52
|
+
|
|
53
|
+
**1.0 lands when the Rust kernel reaches feature parity with the Node.js implementation** — that is the milestone that freezes the manifest contract across runtimes.
|
|
52
54
|
|
|
53
55
|
## The Meaning of Telo
|
|
54
56
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
1
|
+
import type { InvokeContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
4
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
@@ -18,7 +18,7 @@ declare class SqlCommandResource implements ResourceInstance {
|
|
|
18
18
|
private readonly manifest;
|
|
19
19
|
private readonly ctx;
|
|
20
20
|
constructor(manifest: SqlCommandManifest, ctx: ResourceContext);
|
|
21
|
-
invoke(input: unknown): Promise<SqlResult>;
|
|
21
|
+
invoke(input: unknown, invokeCtx?: InvokeContext): Promise<SqlResult>;
|
|
22
22
|
}
|
|
23
23
|
export declare function register(): void;
|
|
24
24
|
export declare function create(resource: SqlCommandManifest, ctx: ResourceContext): Promise<SqlCommandResource>;
|
|
@@ -7,7 +7,7 @@ class SqlCommandResource {
|
|
|
7
7
|
this.manifest = manifest;
|
|
8
8
|
this.ctx = ctx;
|
|
9
9
|
}
|
|
10
|
-
async invoke(input) {
|
|
10
|
+
async invoke(input, invokeCtx) {
|
|
11
11
|
const m = this.manifest;
|
|
12
12
|
const ctx = this.ctx;
|
|
13
13
|
const connection = resolveSqlConnection(m.connection, ctx, () => `Sql.Command "${m.metadata.name}": 'connection'`) ??
|
|
@@ -15,7 +15,12 @@ class SqlCommandResource {
|
|
|
15
15
|
if (!connection) {
|
|
16
16
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
// ERR_ZONE_REQUIRED when no sql.Transaction zone is open on THIS statement's
|
|
19
|
+
// connection — a transaction on another connection no longer answers. The
|
|
20
|
+
// kernel supplies the zone kind and the correlation key (including the
|
|
21
|
+
// `/transaction/connection` fallback) from the `transaction` annotation.
|
|
22
|
+
const zone = m.transaction ? ctx.requireZone("transaction", invokeCtx) : undefined;
|
|
23
|
+
const result = await runSql(connection, zone, input, ctx, invokeCtx);
|
|
19
24
|
return { rows: result.rows, rowCount: connection.toRowCount(result) };
|
|
20
25
|
}
|
|
21
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { type InvokeContext, type ResourceContext, type ZoneEntry } from "@telorun/sdk";
|
|
1
2
|
import { type Kysely, type QueryResult } from "kysely";
|
|
2
3
|
import type { SqlConnection, SqlDialect } from "./sql-connection.js";
|
|
3
|
-
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
4
|
/**
|
|
5
5
|
* The dialect-neutral half of a connection: statement execution, transaction
|
|
6
6
|
* scoping, template binding and row-count normalization over a kysely instance.
|
|
@@ -10,15 +10,18 @@ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
|
10
10
|
* where the driver has a native multi-statement path.
|
|
11
11
|
*/
|
|
12
12
|
export declare abstract class SqlConnectionBase implements SqlConnection {
|
|
13
|
+
#private;
|
|
13
14
|
readonly dialect: SqlDialect;
|
|
15
|
+
protected readonly ctx: ResourceContext;
|
|
14
16
|
protected readonly db: Kysely<any>;
|
|
15
|
-
constructor(db: Kysely<any>, dialect: SqlDialect);
|
|
17
|
+
constructor(db: Kysely<any>, dialect: SqlDialect, ctx: ResourceContext);
|
|
16
18
|
get kysely(): Kysely<any>;
|
|
17
19
|
init(): Promise<void>;
|
|
18
20
|
teardown(): Promise<void>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
runInTransaction<T>(body: (bind: (entry: ZoneEntry) => void) => Promise<T>): Promise<T>;
|
|
22
|
+
hasOpenTransaction(ctx?: InvokeContext): boolean;
|
|
23
|
+
execute<T>(sql: string, params?: unknown[], zone?: ZoneEntry, ctx?: InvokeContext): Promise<QueryResult<T>>;
|
|
24
|
+
executeTemplate<T>(fragments: string[], values: unknown[], zone?: ZoneEntry, ctx?: InvokeContext): Promise<QueryResult<T>>;
|
|
22
25
|
/** Hand the whole script to the driver as one statement. Backends whose driver
|
|
23
26
|
* needs a dedicated multi-statement entry point override this. */
|
|
24
27
|
executeScript(sql: string): Promise<void>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InvokeError, } from "@telorun/sdk";
|
|
2
2
|
import { CompiledQuery } from "kysely";
|
|
3
|
-
import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-store.js";
|
|
4
3
|
/**
|
|
5
4
|
* The dialect-neutral half of a connection: statement execution, transaction
|
|
6
5
|
* scoping, template binding and row-count normalization over a kysely instance.
|
|
@@ -11,9 +10,17 @@ import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-st
|
|
|
11
10
|
*/
|
|
12
11
|
export class SqlConnectionBase {
|
|
13
12
|
dialect;
|
|
13
|
+
ctx;
|
|
14
14
|
db;
|
|
15
|
-
|
|
15
|
+
/** Executors for transaction zones open on THIS connection. An instance
|
|
16
|
+
* field, never a module global: the transaction controller and this base are
|
|
17
|
+
* delivered as different bundles, each inlining its own copy of a shared
|
|
18
|
+
* source file, so module scope would be one map per bundle and every lookup
|
|
19
|
+
* a miss (the payload rule, kernel/specs/execution-zones.md §8). */
|
|
20
|
+
#executors = new WeakMap();
|
|
21
|
+
constructor(db, dialect, ctx) {
|
|
16
22
|
this.dialect = dialect;
|
|
23
|
+
this.ctx = ctx;
|
|
17
24
|
this.db = db;
|
|
18
25
|
}
|
|
19
26
|
get kysely() {
|
|
@@ -27,28 +34,24 @@ export class SqlConnectionBase {
|
|
|
27
34
|
async teardown() {
|
|
28
35
|
await this.db.destroy();
|
|
29
36
|
}
|
|
30
|
-
async
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
finally {
|
|
38
|
-
deleteTx(txId);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
37
|
+
async runInTransaction(body) {
|
|
38
|
+
return this.db
|
|
39
|
+
.transaction()
|
|
40
|
+
.execute((trx) => body((entry) => this.#executors.set(entry, trx)));
|
|
41
|
+
}
|
|
42
|
+
hasOpenTransaction(ctx) {
|
|
43
|
+
return this.ctx.zonesFor(this, ctx).some((entry) => this.#executors.has(entry));
|
|
41
44
|
}
|
|
42
|
-
async execute(sql, params = [],
|
|
43
|
-
const executor = this.resolveExecutor(
|
|
45
|
+
async execute(sql, params = [], zone, ctx) {
|
|
46
|
+
const executor = this.resolveExecutor(zone, ctx);
|
|
44
47
|
return executor.executeQuery(CompiledQuery.raw(sql, params));
|
|
45
48
|
}
|
|
46
|
-
async executeTemplate(fragments, values,
|
|
49
|
+
async executeTemplate(fragments, values, zone, ctx) {
|
|
47
50
|
let sql = fragments[0] ?? "";
|
|
48
51
|
for (let i = 1; i < fragments.length; i++) {
|
|
49
52
|
sql += this.placeholder(i) + fragments[i];
|
|
50
53
|
}
|
|
51
|
-
return this.execute(sql, values,
|
|
54
|
+
return this.execute(sql, values, zone, ctx);
|
|
52
55
|
}
|
|
53
56
|
/** Hand the whole script to the driver as one statement. Backends whose driver
|
|
54
57
|
* needs a dedicated multi-statement entry point override this. */
|
|
@@ -67,17 +70,16 @@ export class SqlConnectionBase {
|
|
|
67
70
|
placeholder(index) {
|
|
68
71
|
return this.dialect.placeholderStyle === "numbered" ? `$${index}` : "?";
|
|
69
72
|
}
|
|
70
|
-
resolveExecutor(
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
+
resolveExecutor(zone, ctx) {
|
|
74
|
+
if (zone && !this.#executors.has(zone)) {
|
|
75
|
+
// A zone this connection did not open cannot be silently ignored: the
|
|
76
|
+
// caller declared a requirement, and `?? this.db` here would execute it
|
|
77
|
+
// outside the transaction it asked for — silent non-transactional writes
|
|
78
|
+
// instead of a loud failure.
|
|
79
|
+
throw new InvokeError("ERR_SQL_ZONE_FOREIGN", `Sql: the ${zone.kind} zone provided by '${zone.provider.ref.name}' was not opened on ` +
|
|
80
|
+
`this connection — the statement would execute outside the transaction it names`);
|
|
73
81
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const entry = getTx(txId);
|
|
77
|
-
if (entry) {
|
|
78
|
-
return entry.executor;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return this.db;
|
|
82
|
+
const entry = zone ?? this.ctx.zonesFor(this, ctx).find((e) => this.#executors.has(e));
|
|
83
|
+
return (entry && this.#executors.get(entry)) ?? this.db;
|
|
82
84
|
}
|
|
83
85
|
}
|
package/dist/sql-connection.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { ResourceInstance } from "@telorun/sdk";
|
|
1
|
+
import type { InvokeContext, ResourceInstance, ZoneEntry } from "@telorun/sdk";
|
|
2
2
|
import type { Kysely, QueryResult } from "kysely";
|
|
3
|
-
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
3
|
/** Native bind-placeholder syntax: SQLite binds anonymous `?`, PostgreSQL binds
|
|
5
4
|
* numbered `$1`, `$2`, … */
|
|
6
5
|
export type PlaceholderStyle = "qmark" | "numbered";
|
|
@@ -25,6 +24,12 @@ export declare function quoteAnsiIdentifier(name: string): string;
|
|
|
25
24
|
* against. Backends (`sql-postgres`, `sql-sqlite`) own their own implementation
|
|
26
25
|
* — usually by extending {@link SqlConnectionBase} — so nothing in this module
|
|
27
26
|
* knows which databases exist.
|
|
27
|
+
*
|
|
28
|
+
* Transaction membership is ambient, carried by the kernel's execution-zone
|
|
29
|
+
* stack (kernel/specs/execution-zones.md) and keyed per connection: a statement
|
|
30
|
+
* executes on an open transaction's executor when a transaction zone
|
|
31
|
+
* correlated on THIS connection is ambient, or when the caller passes the
|
|
32
|
+
* zone entry explicitly.
|
|
28
33
|
*/
|
|
29
34
|
export interface SqlConnection extends ResourceInstance {
|
|
30
35
|
readonly dialect: SqlDialect;
|
|
@@ -33,13 +38,19 @@ export interface SqlConnection extends ResourceInstance {
|
|
|
33
38
|
* must stay implementable by a driver kysely does not support; a consumer
|
|
34
39
|
* that needs it (`Sql.Migrations`) checks and fails with a clear message. */
|
|
35
40
|
readonly kysely?: Kysely<any>;
|
|
36
|
-
execute<T>(sql: string, params?: unknown[],
|
|
41
|
+
execute<T>(sql: string, params?: unknown[], zone?: ZoneEntry, ctx?: InvokeContext): Promise<QueryResult<T>>;
|
|
37
42
|
/** Assemble SQL from literal fragments by interleaving dialect-native
|
|
38
43
|
* placeholders, then bind `values` positionally. */
|
|
39
|
-
executeTemplate<T>(fragments: string[], values: unknown[],
|
|
44
|
+
executeTemplate<T>(fragments: string[], values: unknown[], zone?: ZoneEntry, ctx?: InvokeContext): Promise<QueryResult<T>>;
|
|
40
45
|
/** Run a multi-statement script. */
|
|
41
46
|
executeScript(sql: string): Promise<void>;
|
|
42
|
-
transaction
|
|
47
|
+
/** Open a database transaction and hand the caller a `bind` that keys the
|
|
48
|
+
* open executor on the zone entry the caller mints (via `ctx.withZone`).
|
|
49
|
+
* The executor stays private to this connection — the payload rule. */
|
|
50
|
+
runInTransaction<T>(body: (bind: (entry: ZoneEntry) => void) => Promise<T>, ctx?: InvokeContext): Promise<T>;
|
|
51
|
+
/** True when an ambient transaction zone correlated on this connection has an
|
|
52
|
+
* open executor here — the flat-nesting check `Sql.Transaction` reuses. */
|
|
53
|
+
hasOpenTransaction(ctx?: InvokeContext): boolean;
|
|
43
54
|
/** Rows affected by a write, normalized across drivers. */
|
|
44
55
|
toRowCount(result: QueryResult<unknown>): number;
|
|
45
56
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
1
|
+
import type { InvokeContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
4
|
interface SqlQueryManifest {
|
|
@@ -21,7 +21,7 @@ declare class SqlQueryResource implements ResourceInstance {
|
|
|
21
21
|
private readonly manifest;
|
|
22
22
|
private readonly ctx;
|
|
23
23
|
constructor(manifest: SqlQueryManifest, ctx: ResourceContext);
|
|
24
|
-
invoke(input: unknown): Promise<SqlResult>;
|
|
24
|
+
invoke(input: unknown, invokeCtx?: InvokeContext): Promise<SqlResult>;
|
|
25
25
|
}
|
|
26
26
|
export declare function register(): void;
|
|
27
27
|
export declare function create(resource: SqlQueryManifest, ctx: ResourceContext): Promise<SqlQueryResource>;
|
|
@@ -7,11 +7,14 @@ class SqlQueryResource {
|
|
|
7
7
|
this.manifest = manifest;
|
|
8
8
|
this.ctx = ctx;
|
|
9
9
|
}
|
|
10
|
-
async invoke(input) {
|
|
10
|
+
async invoke(input, invokeCtx) {
|
|
11
11
|
const m = this.manifest;
|
|
12
12
|
const ctx = this.ctx;
|
|
13
13
|
const connection = resolveConnection(m.connection, m.transaction, ctx, () => `Sql.Query "${m.metadata.name}": 'connection'`);
|
|
14
|
-
|
|
14
|
+
// ERR_ZONE_REQUIRED when no sql.Transaction zone is open on THIS query's
|
|
15
|
+
// connection — a transaction on another connection no longer answers.
|
|
16
|
+
const zone = m.transaction ? ctx.requireZone("transaction", invokeCtx) : undefined;
|
|
17
|
+
const result = await runSql(connection, zone, input, ctx, invokeCtx);
|
|
15
18
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
16
19
|
}
|
|
17
20
|
}
|
package/dist/sql-run.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { type ResourceContext } from "@telorun/sdk";
|
|
1
|
+
import { type InvokeContext, type ResourceContext, type ZoneEntry } from "@telorun/sdk";
|
|
2
2
|
import type { QueryResult } from "kysely";
|
|
3
3
|
import type { SqlConnection } from "./sql-connection.js";
|
|
4
|
-
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
4
|
/** Execute the `sql` input of a Query/Exec resource against `connection`.
|
|
6
5
|
*
|
|
7
6
|
* Two modes:
|
|
@@ -13,4 +12,4 @@ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
|
13
12
|
*
|
|
14
13
|
* Mixing a `!sql` template with `bindings` is rejected. A plain string `sql`
|
|
15
14
|
* with neither is executed verbatim. */
|
|
16
|
-
export declare function runSql(connection: SqlConnection,
|
|
15
|
+
export declare function runSql(connection: SqlConnection, zone: ZoneEntry | undefined, input: unknown, ctx: ResourceContext, invokeCtx?: InvokeContext): Promise<QueryResult<Record<string, unknown>>>;
|
package/dist/sql-run.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InvokeError, isParameterizedSql } from "@telorun/sdk";
|
|
1
|
+
import { InvokeError, isParameterizedSql, } from "@telorun/sdk";
|
|
2
2
|
/** Execute the `sql` input of a Query/Exec resource against `connection`.
|
|
3
3
|
*
|
|
4
4
|
* Two modes:
|
|
@@ -10,7 +10,7 @@ import { InvokeError, isParameterizedSql } from "@telorun/sdk";
|
|
|
10
10
|
*
|
|
11
11
|
* Mixing a `!sql` template with `bindings` is rejected. A plain string `sql`
|
|
12
12
|
* with neither is executed verbatim. */
|
|
13
|
-
export async function runSql(connection,
|
|
13
|
+
export async function runSql(connection, zone, input, ctx, invokeCtx) {
|
|
14
14
|
const expanded = ctx.expandValue(input, {});
|
|
15
15
|
const sql = expanded.sql;
|
|
16
16
|
const bindings = expanded.bindings;
|
|
@@ -21,7 +21,7 @@ export async function runSql(connection, transaction, input, ctx) {
|
|
|
21
21
|
"Use one or the other — `!sql` binds each inline value automatically; " +
|
|
22
22
|
"`bindings` is for hand-written ? / $n placeholders.");
|
|
23
23
|
}
|
|
24
|
-
return connection.executeTemplate(sql.fragments, sql.values,
|
|
24
|
+
return connection.executeTemplate(sql.fragments, sql.values, zone, invokeCtx);
|
|
25
25
|
}
|
|
26
|
-
return connection.execute(sql, hasBindings ? bindings : [],
|
|
26
|
+
return connection.execute(sql, hasBindings ? bindings : [], zone, invokeCtx);
|
|
27
27
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
1
|
+
import type { InvokeContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
4
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
@@ -62,7 +62,7 @@ declare class SqlSelectionResource implements ResourceInstance {
|
|
|
62
62
|
private readonly manifest;
|
|
63
63
|
private readonly ctx;
|
|
64
64
|
constructor(manifest: SelectManifest, ctx: ResourceContext);
|
|
65
|
-
invoke(input: unknown): Promise<SqlResult>;
|
|
65
|
+
invoke(input: unknown, invokeCtx?: InvokeContext): Promise<SqlResult>;
|
|
66
66
|
}
|
|
67
67
|
export declare function register(): void;
|
|
68
68
|
export declare function create(resource: SelectManifest, ctx: ResourceContext): Promise<SqlSelectionResource>;
|
|
@@ -7,7 +7,7 @@ class SqlSelectionResource {
|
|
|
7
7
|
this.manifest = manifest;
|
|
8
8
|
this.ctx = ctx;
|
|
9
9
|
}
|
|
10
|
-
async invoke(input) {
|
|
10
|
+
async invoke(input, invokeCtx) {
|
|
11
11
|
const m = this.manifest;
|
|
12
12
|
const ctx = this.ctx;
|
|
13
13
|
const inputs = {
|
|
@@ -24,8 +24,11 @@ class SqlSelectionResource {
|
|
|
24
24
|
if (!connection) {
|
|
25
25
|
throw new Error("Sql.Selection: either 'connection' or 'transaction' must be set");
|
|
26
26
|
}
|
|
27
|
+
// ERR_ZONE_REQUIRED when no sql.Transaction zone is open on THIS selection's
|
|
28
|
+
// connection — a transaction on another connection no longer answers.
|
|
29
|
+
const zone = m.transaction ? ctx.requireZone("transaction", invokeCtx) : undefined;
|
|
27
30
|
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.dialect);
|
|
28
|
-
const result = await connection.execute(sql, params,
|
|
31
|
+
const result = await connection.execute(sql, params, zone, invokeCtx);
|
|
29
32
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InvokeContext, type ResourceContext, type ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
interface SqlTransactionManifest {
|
|
4
4
|
metadata: {
|
|
@@ -6,7 +6,7 @@ interface SqlTransactionManifest {
|
|
|
6
6
|
module: string;
|
|
7
7
|
};
|
|
8
8
|
connection: SqlConnection;
|
|
9
|
-
steps:
|
|
9
|
+
steps: ResourceInstance;
|
|
10
10
|
inputs?: Record<string, unknown>;
|
|
11
11
|
}
|
|
12
12
|
export declare class SqlTransactionResource implements ResourceInstance {
|
|
@@ -14,8 +14,8 @@ export declare class SqlTransactionResource implements ResourceInstance {
|
|
|
14
14
|
private readonly ctx;
|
|
15
15
|
constructor(manifest: SqlTransactionManifest, ctx: ResourceContext);
|
|
16
16
|
getConnection(): SqlConnection;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
invoke(input: unknown, invokeCtx?: InvokeContext): Promise<unknown>;
|
|
18
|
+
private dispatchSteps;
|
|
19
19
|
}
|
|
20
20
|
export declare function register(): void;
|
|
21
21
|
export declare function create(resource: SqlTransactionManifest, ctx: ResourceContext): Promise<SqlTransactionResource>;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { InvokeError, getRefIdentity, } from "@telorun/sdk";
|
|
1
2
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
2
|
-
|
|
3
|
+
/** The steps slot is `Telo.Executable`: an instance with either entry point. */
|
|
4
|
+
function isExecutable(value) {
|
|
5
|
+
const v = value;
|
|
6
|
+
return typeof v?.invoke === "function" || typeof v?.run === "function";
|
|
7
|
+
}
|
|
3
8
|
export class SqlTransactionResource {
|
|
4
9
|
manifest;
|
|
5
10
|
ctx;
|
|
@@ -10,22 +15,49 @@ export class SqlTransactionResource {
|
|
|
10
15
|
getConnection() {
|
|
11
16
|
return (resolveSqlConnection(this.manifest.connection, this.ctx, () => `Sql.Transaction "${this.manifest.metadata.name}": 'connection'`) ?? failMissingConnection(this.manifest.metadata.name));
|
|
12
17
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
async invoke(input, invokeCtx) {
|
|
19
|
+
const m = this.manifest;
|
|
20
|
+
const ctx = this.ctx;
|
|
21
|
+
// The declared `inputs:` map's CEL reads the caller's invocation input as
|
|
22
|
+
// `inputs.<field>` — the same variable name a Run.Sequence step's inputs
|
|
23
|
+
// read, and what the slot's `inputs: /inputs` pointer names.
|
|
24
|
+
const celScope = { inputs: input ?? {} };
|
|
25
|
+
const expandedInputs = ctx.expandValue(m.inputs ?? {}, celScope);
|
|
26
|
+
// `invokeCtx` is threaded rather than left to the ambient throughout: the
|
|
27
|
+
// caller's context is the authority on which zones are open, and a runtime
|
|
28
|
+
// with no ambient store has nothing else to read.
|
|
29
|
+
//
|
|
30
|
+
// Flat nesting: an ambient transaction on the SAME connection is joined; a
|
|
31
|
+
// different connection's transaction is not ours and we open our own.
|
|
32
|
+
const conn = this.getConnection();
|
|
33
|
+
if (conn.hasOpenTransaction(invokeCtx)) {
|
|
34
|
+
return this.dispatchSteps(expandedInputs, invokeCtx);
|
|
16
35
|
}
|
|
36
|
+
return conn.runInTransaction((bind) =>
|
|
37
|
+
// "steps" is this kind's own slot; the kernel reads `x-telo-provides-zone`
|
|
38
|
+
// there for the zone kind (this kind) and the correlation key
|
|
39
|
+
// (`/connection`). The entry is handed to the connection's own map —
|
|
40
|
+
// across the bundle/npm delivery split — and the derived context is
|
|
41
|
+
// threaded into the body dispatch, the discipline cancellation has.
|
|
42
|
+
ctx.withZone("steps", (zoneCtx, entry) => {
|
|
43
|
+
bind(entry);
|
|
44
|
+
return this.dispatchSteps(expandedInputs, zoneCtx);
|
|
45
|
+
}, invokeCtx));
|
|
17
46
|
}
|
|
18
|
-
|
|
47
|
+
dispatchSteps(inputs, zoneCtx) {
|
|
19
48
|
const m = this.manifest;
|
|
20
|
-
const
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
49
|
+
const target = this.ctx.resolveRef(m.steps, isExecutable, () => `Sql.Transaction "${m.metadata.name}": 'steps'`, "Telo.Executable");
|
|
50
|
+
// The kernel stamps `!ref` identity at Phase-5 injection, and `resolveRef`
|
|
51
|
+
// rescues the sentinel form, so a resolved target always carries one.
|
|
52
|
+
// Guessing a label instead would emit malformed dispatch events (`.Invoked`
|
|
53
|
+
// with no kind) — a silent wrong answer where a missing identity means the
|
|
54
|
+
// resolution path changed under us.
|
|
55
|
+
const id = getRefIdentity(target);
|
|
56
|
+
if (!id) {
|
|
57
|
+
throw new InvokeError("ERR_SQL_STEPS_UNIDENTIFIED", `Sql.Transaction '${m.metadata.name}': the resolved 'steps' target carries no reference ` +
|
|
58
|
+
`identity, so its dispatch cannot be traced or named`);
|
|
25
59
|
}
|
|
26
|
-
|
|
27
|
-
const expandedInputs = ctx.expandValue(m.inputs ?? {}, input ?? {});
|
|
28
|
-
return conn.transaction(() => m.steps.invoke(expandedInputs));
|
|
60
|
+
return this.ctx.invokeResolved(id.kind, id.name, target, inputs, zoneCtx);
|
|
29
61
|
}
|
|
30
62
|
}
|
|
31
63
|
function failMissingConnection(name) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -26,33 +26,10 @@
|
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
27
|
"exports": {
|
|
28
28
|
".": {
|
|
29
|
+
"source": "./src/index.ts",
|
|
29
30
|
"types": "./dist/index.d.ts",
|
|
30
31
|
"bun": "./src/index.ts",
|
|
31
32
|
"import": "./dist/index.js"
|
|
32
|
-
},
|
|
33
|
-
"./sql-query": {
|
|
34
|
-
"bun": "./src/sql-query-controller.ts",
|
|
35
|
-
"import": "./dist/sql-query-controller.js"
|
|
36
|
-
},
|
|
37
|
-
"./sql-selection": {
|
|
38
|
-
"bun": "./src/sql-selection-controller.ts",
|
|
39
|
-
"import": "./dist/sql-selection-controller.js"
|
|
40
|
-
},
|
|
41
|
-
"./sql-command": {
|
|
42
|
-
"bun": "./src/sql-command-controller.ts",
|
|
43
|
-
"import": "./dist/sql-command-controller.js"
|
|
44
|
-
},
|
|
45
|
-
"./sql-transaction": {
|
|
46
|
-
"bun": "./src/sql-transaction-controller.ts",
|
|
47
|
-
"import": "./dist/sql-transaction-controller.js"
|
|
48
|
-
},
|
|
49
|
-
"./sql-migration": {
|
|
50
|
-
"bun": "./src/sql-migration-controller.ts",
|
|
51
|
-
"import": "./dist/sql-migration-controller.js"
|
|
52
|
-
},
|
|
53
|
-
"./sql-migrations": {
|
|
54
|
-
"bun": "./src/sql-migrations-controller.ts",
|
|
55
|
-
"import": "./dist/sql-migrations-controller.js"
|
|
56
33
|
}
|
|
57
34
|
},
|
|
58
35
|
"files": [
|
|
@@ -64,13 +41,14 @@
|
|
|
64
41
|
},
|
|
65
42
|
"devDependencies": {
|
|
66
43
|
"@types/node": "^20.0.0",
|
|
44
|
+
"esbuild": "^0.25.12",
|
|
67
45
|
"typescript": "^5.0.0",
|
|
68
|
-
"@telorun/sdk": "0.
|
|
46
|
+
"@telorun/sdk": "0.67.0"
|
|
69
47
|
},
|
|
70
48
|
"peerDependencies": {
|
|
71
49
|
"@telorun/sdk": "*"
|
|
72
50
|
},
|
|
73
51
|
"scripts": {
|
|
74
|
-
"build": "tsc -p tsconfig.lib.json"
|
|
52
|
+
"build": "tsc -p tsconfig.lib.json && esbuild src/sql-command-controller.ts src/sql-migration-controller.ts src/sql-migrations-controller.ts src/sql-query-controller.ts src/sql-selection-controller.ts src/sql-transaction-controller.ts --bundle --format=esm --platform=node --target=node20 --external:@telorun/sdk --conditions=source --outdir=. --banner:js='import { createRequire as __teloCreateRequire } from \"node:module\";const require = __teloCreateRequire(import.meta.url);' --out-extension:.js=.mjs"
|
|
75
53
|
}
|
|
76
54
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
1
|
+
import type { InvokeContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
4
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
@@ -21,7 +21,7 @@ class SqlCommandResource implements ResourceInstance {
|
|
|
21
21
|
private readonly ctx: ResourceContext,
|
|
22
22
|
) {}
|
|
23
23
|
|
|
24
|
-
async invoke(input: unknown): Promise<SqlResult> {
|
|
24
|
+
async invoke(input: unknown, invokeCtx?: InvokeContext): Promise<SqlResult> {
|
|
25
25
|
const m = this.manifest;
|
|
26
26
|
const ctx = this.ctx;
|
|
27
27
|
|
|
@@ -32,7 +32,12 @@ class SqlCommandResource implements ResourceInstance {
|
|
|
32
32
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// ERR_ZONE_REQUIRED when no sql.Transaction zone is open on THIS statement's
|
|
36
|
+
// connection — a transaction on another connection no longer answers. The
|
|
37
|
+
// kernel supplies the zone kind and the correlation key (including the
|
|
38
|
+
// `/transaction/connection` fallback) from the `transaction` annotation.
|
|
39
|
+
const zone = m.transaction ? ctx.requireZone("transaction", invokeCtx) : undefined;
|
|
40
|
+
const result = await runSql(connection, zone, input, ctx, invokeCtx);
|
|
36
41
|
return { rows: result.rows, rowCount: connection.toRowCount(result) };
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
InvokeError,
|
|
3
|
+
type InvokeContext,
|
|
4
|
+
type ResourceContext,
|
|
5
|
+
type ZoneEntry,
|
|
6
|
+
} from "@telorun/sdk";
|
|
2
7
|
import { CompiledQuery, type Kysely, type QueryResult, type Transaction } from "kysely";
|
|
3
8
|
import type { SqlConnection, SqlDialect } from "./sql-connection.js";
|
|
4
|
-
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
|
-
import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-store.js";
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* The dialect-neutral half of a connection: statement execution, transaction
|
|
@@ -15,9 +18,17 @@ import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-st
|
|
|
15
18
|
export abstract class SqlConnectionBase implements SqlConnection {
|
|
16
19
|
protected readonly db: Kysely<any>;
|
|
17
20
|
|
|
21
|
+
/** Executors for transaction zones open on THIS connection. An instance
|
|
22
|
+
* field, never a module global: the transaction controller and this base are
|
|
23
|
+
* delivered as different bundles, each inlining its own copy of a shared
|
|
24
|
+
* source file, so module scope would be one map per bundle and every lookup
|
|
25
|
+
* a miss (the payload rule, kernel/specs/execution-zones.md §8). */
|
|
26
|
+
readonly #executors = new WeakMap<ZoneEntry, Kysely<any>>();
|
|
27
|
+
|
|
18
28
|
constructor(
|
|
19
29
|
db: Kysely<any>,
|
|
20
30
|
readonly dialect: SqlDialect,
|
|
31
|
+
protected readonly ctx: ResourceContext,
|
|
21
32
|
) {
|
|
22
33
|
this.db = db;
|
|
23
34
|
}
|
|
@@ -36,38 +47,39 @@ export abstract class SqlConnectionBase implements SqlConnection {
|
|
|
36
47
|
await this.db.destroy();
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
async
|
|
40
|
-
|
|
50
|
+
async runInTransaction<T>(
|
|
51
|
+
body: (bind: (entry: ZoneEntry) => void) => Promise<T>,
|
|
52
|
+
): Promise<T> {
|
|
53
|
+
return this.db
|
|
54
|
+
.transaction()
|
|
55
|
+
.execute((trx: Transaction<any>) => body((entry) => this.#executors.set(entry, trx)));
|
|
56
|
+
}
|
|
41
57
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
return await txStorage.run(txId, cb);
|
|
46
|
-
} finally {
|
|
47
|
-
deleteTx(txId);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
58
|
+
hasOpenTransaction(ctx?: InvokeContext): boolean {
|
|
59
|
+
return this.ctx.zonesFor(this, ctx).some((entry) => this.#executors.has(entry));
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
async execute<T>(
|
|
53
63
|
sql: string,
|
|
54
64
|
params: unknown[] = [],
|
|
55
|
-
|
|
65
|
+
zone?: ZoneEntry,
|
|
66
|
+
ctx?: InvokeContext,
|
|
56
67
|
): Promise<QueryResult<T>> {
|
|
57
|
-
const executor = this.resolveExecutor(
|
|
68
|
+
const executor = this.resolveExecutor(zone, ctx);
|
|
58
69
|
return executor.executeQuery<T>(CompiledQuery.raw(sql, params));
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
async executeTemplate<T>(
|
|
62
73
|
fragments: string[],
|
|
63
74
|
values: unknown[],
|
|
64
|
-
|
|
75
|
+
zone?: ZoneEntry,
|
|
76
|
+
ctx?: InvokeContext,
|
|
65
77
|
): Promise<QueryResult<T>> {
|
|
66
78
|
let sql = fragments[0] ?? "";
|
|
67
79
|
for (let i = 1; i < fragments.length; i++) {
|
|
68
80
|
sql += this.placeholder(i) + fragments[i];
|
|
69
81
|
}
|
|
70
|
-
return this.execute<T>(sql, values,
|
|
82
|
+
return this.execute<T>(sql, values, zone, ctx);
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
/** Hand the whole script to the driver as one statement. Backends whose driver
|
|
@@ -92,19 +104,19 @@ export abstract class SqlConnectionBase implements SqlConnection {
|
|
|
92
104
|
return this.dialect.placeholderStyle === "numbered" ? `$${index}` : "?";
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
private resolveExecutor(
|
|
96
|
-
if (
|
|
97
|
-
|
|
107
|
+
private resolveExecutor(zone?: ZoneEntry, ctx?: InvokeContext): Kysely<any> {
|
|
108
|
+
if (zone && !this.#executors.has(zone)) {
|
|
109
|
+
// A zone this connection did not open cannot be silently ignored: the
|
|
110
|
+
// caller declared a requirement, and `?? this.db` here would execute it
|
|
111
|
+
// outside the transaction it asked for — silent non-transactional writes
|
|
112
|
+
// instead of a loud failure.
|
|
113
|
+
throw new InvokeError(
|
|
114
|
+
"ERR_SQL_ZONE_FOREIGN",
|
|
115
|
+
`Sql: the ${zone.kind} zone provided by '${zone.provider.ref.name}' was not opened on ` +
|
|
116
|
+
`this connection — the statement would execute outside the transaction it names`,
|
|
117
|
+
);
|
|
98
118
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (txId) {
|
|
102
|
-
const entry = getTx(txId);
|
|
103
|
-
if (entry) {
|
|
104
|
-
return entry.executor as Kysely<any>;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return this.db;
|
|
119
|
+
const entry = zone ?? this.ctx.zonesFor(this, ctx).find((e) => this.#executors.has(e));
|
|
120
|
+
return (entry && this.#executors.get(entry)) ?? this.db;
|
|
109
121
|
}
|
|
110
122
|
}
|
package/src/sql-connection.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { ResourceInstance } from "@telorun/sdk";
|
|
1
|
+
import type { InvokeContext, ResourceInstance, ZoneEntry } from "@telorun/sdk";
|
|
2
2
|
import type { Kysely, QueryResult } from "kysely";
|
|
3
|
-
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
3
|
|
|
5
4
|
/** Native bind-placeholder syntax: SQLite binds anonymous `?`, PostgreSQL binds
|
|
6
5
|
* numbered `$1`, `$2`, … */
|
|
@@ -37,6 +36,12 @@ export function quoteAnsiIdentifier(name: string): string {
|
|
|
37
36
|
* against. Backends (`sql-postgres`, `sql-sqlite`) own their own implementation
|
|
38
37
|
* — usually by extending {@link SqlConnectionBase} — so nothing in this module
|
|
39
38
|
* knows which databases exist.
|
|
39
|
+
*
|
|
40
|
+
* Transaction membership is ambient, carried by the kernel's execution-zone
|
|
41
|
+
* stack (kernel/specs/execution-zones.md) and keyed per connection: a statement
|
|
42
|
+
* executes on an open transaction's executor when a transaction zone
|
|
43
|
+
* correlated on THIS connection is ambient, or when the caller passes the
|
|
44
|
+
* zone entry explicitly.
|
|
40
45
|
*/
|
|
41
46
|
export interface SqlConnection extends ResourceInstance {
|
|
42
47
|
readonly dialect: SqlDialect;
|
|
@@ -50,7 +55,8 @@ export interface SqlConnection extends ResourceInstance {
|
|
|
50
55
|
execute<T>(
|
|
51
56
|
sql: string,
|
|
52
57
|
params?: unknown[],
|
|
53
|
-
|
|
58
|
+
zone?: ZoneEntry,
|
|
59
|
+
ctx?: InvokeContext,
|
|
54
60
|
): Promise<QueryResult<T>>;
|
|
55
61
|
|
|
56
62
|
/** Assemble SQL from literal fragments by interleaving dialect-native
|
|
@@ -58,13 +64,24 @@ export interface SqlConnection extends ResourceInstance {
|
|
|
58
64
|
executeTemplate<T>(
|
|
59
65
|
fragments: string[],
|
|
60
66
|
values: unknown[],
|
|
61
|
-
|
|
67
|
+
zone?: ZoneEntry,
|
|
68
|
+
ctx?: InvokeContext,
|
|
62
69
|
): Promise<QueryResult<T>>;
|
|
63
70
|
|
|
64
71
|
/** Run a multi-statement script. */
|
|
65
72
|
executeScript(sql: string): Promise<void>;
|
|
66
73
|
|
|
67
|
-
transaction
|
|
74
|
+
/** Open a database transaction and hand the caller a `bind` that keys the
|
|
75
|
+
* open executor on the zone entry the caller mints (via `ctx.withZone`).
|
|
76
|
+
* The executor stays private to this connection — the payload rule. */
|
|
77
|
+
runInTransaction<T>(
|
|
78
|
+
body: (bind: (entry: ZoneEntry) => void) => Promise<T>,
|
|
79
|
+
ctx?: InvokeContext,
|
|
80
|
+
): Promise<T>;
|
|
81
|
+
|
|
82
|
+
/** True when an ambient transaction zone correlated on this connection has an
|
|
83
|
+
* open executor here — the flat-nesting check `Sql.Transaction` reuses. */
|
|
84
|
+
hasOpenTransaction(ctx?: InvokeContext): boolean;
|
|
68
85
|
|
|
69
86
|
/** Rows affected by a write, normalized across drivers. */
|
|
70
87
|
toRowCount(result: QueryResult<unknown>): number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
1
|
+
import type { InvokeContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
4
|
import { runSql } from "./sql-run.js";
|
|
@@ -25,7 +25,7 @@ class SqlQueryResource implements ResourceInstance {
|
|
|
25
25
|
private readonly ctx: ResourceContext,
|
|
26
26
|
) {}
|
|
27
27
|
|
|
28
|
-
async invoke(input: unknown): Promise<SqlResult> {
|
|
28
|
+
async invoke(input: unknown, invokeCtx?: InvokeContext): Promise<SqlResult> {
|
|
29
29
|
const m = this.manifest;
|
|
30
30
|
const ctx = this.ctx;
|
|
31
31
|
const connection = resolveConnection(
|
|
@@ -34,7 +34,10 @@ class SqlQueryResource implements ResourceInstance {
|
|
|
34
34
|
ctx,
|
|
35
35
|
() => `Sql.Query "${m.metadata.name}": 'connection'`,
|
|
36
36
|
);
|
|
37
|
-
|
|
37
|
+
// ERR_ZONE_REQUIRED when no sql.Transaction zone is open on THIS query's
|
|
38
|
+
// connection — a transaction on another connection no longer answers.
|
|
39
|
+
const zone = m.transaction ? ctx.requireZone("transaction", invokeCtx) : undefined;
|
|
40
|
+
const result = await runSql(connection, zone, input, ctx, invokeCtx);
|
|
38
41
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
39
42
|
}
|
|
40
43
|
}
|
package/src/sql-run.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
InvokeError,
|
|
3
|
+
isParameterizedSql,
|
|
4
|
+
type InvokeContext,
|
|
5
|
+
type ResourceContext,
|
|
6
|
+
type ZoneEntry,
|
|
7
|
+
} from "@telorun/sdk";
|
|
2
8
|
import type { QueryResult } from "kysely";
|
|
3
9
|
import type { SqlConnection } from "./sql-connection.js";
|
|
4
|
-
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
10
|
|
|
6
11
|
/** Execute the `sql` input of a Query/Exec resource against `connection`.
|
|
7
12
|
*
|
|
@@ -16,9 +21,10 @@ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
|
16
21
|
* with neither is executed verbatim. */
|
|
17
22
|
export async function runSql(
|
|
18
23
|
connection: SqlConnection,
|
|
19
|
-
|
|
24
|
+
zone: ZoneEntry | undefined,
|
|
20
25
|
input: unknown,
|
|
21
26
|
ctx: ResourceContext,
|
|
27
|
+
invokeCtx?: InvokeContext,
|
|
22
28
|
): Promise<QueryResult<Record<string, unknown>>> {
|
|
23
29
|
const expanded = ctx.expandValue(input, {}) as { sql: unknown; bindings?: unknown[] };
|
|
24
30
|
const sql = expanded.sql;
|
|
@@ -37,13 +43,15 @@ export async function runSql(
|
|
|
37
43
|
return connection.executeTemplate<Record<string, unknown>>(
|
|
38
44
|
sql.fragments,
|
|
39
45
|
sql.values,
|
|
40
|
-
|
|
46
|
+
zone,
|
|
47
|
+
invokeCtx,
|
|
41
48
|
);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
return connection.execute<Record<string, unknown>>(
|
|
45
52
|
sql as string,
|
|
46
53
|
hasBindings ? (bindings as unknown[]) : [],
|
|
47
|
-
|
|
54
|
+
zone,
|
|
55
|
+
invokeCtx,
|
|
48
56
|
);
|
|
49
57
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
1
|
+
import type { InvokeContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import type { SqlConnection, SqlDialect } from "./sql-connection.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
4
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
@@ -82,7 +82,7 @@ class SqlSelectionResource implements ResourceInstance {
|
|
|
82
82
|
private readonly ctx: ResourceContext,
|
|
83
83
|
) {}
|
|
84
84
|
|
|
85
|
-
async invoke(input: unknown): Promise<SqlResult> {
|
|
85
|
+
async invoke(input: unknown, invokeCtx?: InvokeContext): Promise<SqlResult> {
|
|
86
86
|
const m = this.manifest;
|
|
87
87
|
const ctx = this.ctx;
|
|
88
88
|
const inputs = {
|
|
@@ -103,8 +103,11 @@ class SqlSelectionResource implements ResourceInstance {
|
|
|
103
103
|
throw new Error("Sql.Selection: either 'connection' or 'transaction' must be set");
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// ERR_ZONE_REQUIRED when no sql.Transaction zone is open on THIS selection's
|
|
107
|
+
// connection — a transaction on another connection no longer answers.
|
|
108
|
+
const zone = m.transaction ? ctx.requireZone("transaction", invokeCtx) : undefined;
|
|
106
109
|
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.dialect);
|
|
107
|
-
const result = await connection.execute<Record<string, unknown>>(sql, params,
|
|
110
|
+
const result = await connection.execute<Record<string, unknown>>(sql, params, zone, invokeCtx);
|
|
108
111
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
109
112
|
}
|
|
110
113
|
}
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
InvokeError,
|
|
3
|
+
getRefIdentity,
|
|
4
|
+
type InvokeContext,
|
|
5
|
+
type ResourceContext,
|
|
6
|
+
type ResourceInstance,
|
|
7
|
+
} from "@telorun/sdk";
|
|
2
8
|
import type { SqlConnection } from "./sql-connection.js";
|
|
3
9
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
|
-
import { currentTxId } from "./transaction-store.js";
|
|
5
10
|
|
|
6
11
|
interface SqlTransactionManifest {
|
|
7
12
|
metadata: { name: string; module: string };
|
|
8
13
|
connection: SqlConnection;
|
|
9
|
-
steps:
|
|
14
|
+
steps: ResourceInstance;
|
|
10
15
|
inputs?: Record<string, unknown>;
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
/** The steps slot is `Telo.Executable`: an instance with either entry point. */
|
|
19
|
+
function isExecutable(value: unknown): value is ResourceInstance {
|
|
20
|
+
const v = value as ResourceInstance | undefined;
|
|
21
|
+
return typeof v?.invoke === "function" || typeof v?.run === "function";
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export class SqlTransactionResource implements ResourceInstance {
|
|
14
25
|
constructor(
|
|
15
26
|
private readonly manifest: SqlTransactionManifest,
|
|
@@ -26,28 +37,66 @@ export class SqlTransactionResource implements ResourceInstance {
|
|
|
26
37
|
);
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
if (!currentTxId()) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Sql.Transaction '${this.manifest.metadata.name}': used outside an active transaction`,
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async invoke(input: unknown): Promise<unknown> {
|
|
40
|
+
async invoke(input: unknown, invokeCtx?: InvokeContext): Promise<unknown> {
|
|
38
41
|
const m = this.manifest;
|
|
39
42
|
const ctx = this.ctx;
|
|
40
43
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
44
|
+
// The declared `inputs:` map's CEL reads the caller's invocation input as
|
|
45
|
+
// `inputs.<field>` — the same variable name a Run.Sequence step's inputs
|
|
46
|
+
// read, and what the slot's `inputs: /inputs` pointer names.
|
|
47
|
+
const celScope = { inputs: input ?? {} };
|
|
48
|
+
const expandedInputs = ctx.expandValue(m.inputs ?? {}, celScope) as Record<string, unknown>;
|
|
46
49
|
|
|
50
|
+
// `invokeCtx` is threaded rather than left to the ambient throughout: the
|
|
51
|
+
// caller's context is the authority on which zones are open, and a runtime
|
|
52
|
+
// with no ambient store has nothing else to read.
|
|
53
|
+
//
|
|
54
|
+
// Flat nesting: an ambient transaction on the SAME connection is joined; a
|
|
55
|
+
// different connection's transaction is not ours and we open our own.
|
|
47
56
|
const conn = this.getConnection();
|
|
48
|
-
|
|
57
|
+
if (conn.hasOpenTransaction(invokeCtx)) {
|
|
58
|
+
return this.dispatchSteps(expandedInputs, invokeCtx);
|
|
59
|
+
}
|
|
49
60
|
|
|
50
|
-
return conn.
|
|
61
|
+
return conn.runInTransaction((bind) =>
|
|
62
|
+
// "steps" is this kind's own slot; the kernel reads `x-telo-provides-zone`
|
|
63
|
+
// there for the zone kind (this kind) and the correlation key
|
|
64
|
+
// (`/connection`). The entry is handed to the connection's own map —
|
|
65
|
+
// across the bundle/npm delivery split — and the derived context is
|
|
66
|
+
// threaded into the body dispatch, the discipline cancellation has.
|
|
67
|
+
ctx.withZone(
|
|
68
|
+
"steps",
|
|
69
|
+
(zoneCtx, entry) => {
|
|
70
|
+
bind(entry);
|
|
71
|
+
return this.dispatchSteps(expandedInputs, zoneCtx);
|
|
72
|
+
},
|
|
73
|
+
invokeCtx,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private dispatchSteps(inputs: Record<string, unknown>, zoneCtx?: InvokeContext): Promise<unknown> {
|
|
79
|
+
const m = this.manifest;
|
|
80
|
+
const target = this.ctx.resolveRef(
|
|
81
|
+
m.steps,
|
|
82
|
+
isExecutable,
|
|
83
|
+
() => `Sql.Transaction "${m.metadata.name}": 'steps'`,
|
|
84
|
+
"Telo.Executable",
|
|
85
|
+
);
|
|
86
|
+
// The kernel stamps `!ref` identity at Phase-5 injection, and `resolveRef`
|
|
87
|
+
// rescues the sentinel form, so a resolved target always carries one.
|
|
88
|
+
// Guessing a label instead would emit malformed dispatch events (`.Invoked`
|
|
89
|
+
// with no kind) — a silent wrong answer where a missing identity means the
|
|
90
|
+
// resolution path changed under us.
|
|
91
|
+
const id = getRefIdentity(target as object);
|
|
92
|
+
if (!id) {
|
|
93
|
+
throw new InvokeError(
|
|
94
|
+
"ERR_SQL_STEPS_UNIDENTIFIED",
|
|
95
|
+
`Sql.Transaction '${m.metadata.name}': the resolved 'steps' target carries no reference ` +
|
|
96
|
+
`identity, so its dispatch cannot be traced or named`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return this.ctx.invokeResolved(id.kind, id.name, target, inputs, zoneCtx);
|
|
51
100
|
}
|
|
52
101
|
}
|
|
53
102
|
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
export interface TxEntry {
|
|
3
|
-
executor: unknown;
|
|
4
|
-
}
|
|
5
|
-
export declare const txStorage: AsyncLocalStorage<string>;
|
|
6
|
-
export declare const setTx: (id: string, entry: TxEntry) => void;
|
|
7
|
-
export declare const getTx: (id: string) => TxEntry | undefined;
|
|
8
|
-
export declare const deleteTx: (id: string) => void;
|
|
9
|
-
export declare const currentTxId: () => string | undefined;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
const txMap = new Map();
|
|
3
|
-
export const txStorage = new AsyncLocalStorage();
|
|
4
|
-
export const setTx = (id, entry) => {
|
|
5
|
-
txMap.set(id, entry);
|
|
6
|
-
};
|
|
7
|
-
export const getTx = (id) => txMap.get(id);
|
|
8
|
-
export const deleteTx = (id) => {
|
|
9
|
-
txMap.delete(id);
|
|
10
|
-
};
|
|
11
|
-
export const currentTxId = () => txStorage.getStore();
|
package/src/transaction-store.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
|
|
3
|
-
export interface TxEntry {
|
|
4
|
-
executor: unknown;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const txMap = new Map<string, TxEntry>();
|
|
8
|
-
export const txStorage = new AsyncLocalStorage<string>();
|
|
9
|
-
|
|
10
|
-
export const setTx = (id: string, entry: TxEntry): void => {
|
|
11
|
-
txMap.set(id, entry);
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export const getTx = (id: string): TxEntry | undefined => txMap.get(id);
|
|
15
|
-
|
|
16
|
-
export const deleteTx = (id: string): void => {
|
|
17
|
-
txMap.delete(id);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const currentTxId = (): string | undefined => txStorage.getStore();
|