@telorun/sql 0.1.1 → 0.1.3

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.
@@ -0,0 +1,169 @@
1
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import { randomUUID } from "crypto";
3
+ import {
4
+ CompiledQuery,
5
+ Kysely,
6
+ PostgresDialect,
7
+ SqliteDialect,
8
+ type QueryResult,
9
+ type Transaction,
10
+ } from "kysely";
11
+ import { Pool } from "pg";
12
+ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
13
+ import type { SqliteDb } from "./sqlite-driver-interface.js";
14
+ import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-store.js";
15
+
16
+ interface PoolConfig {
17
+ min?: number;
18
+ max?: number;
19
+ idleTimeoutMs?: number;
20
+ connectionTimeoutMs?: number;
21
+ }
22
+
23
+ interface SqlConnectionManifest {
24
+ metadata: { name: string; module: string };
25
+ driver: "postgres" | "sqlite";
26
+ connectionString?: string;
27
+ host?: string;
28
+ port?: number;
29
+ database?: string;
30
+ user?: string;
31
+ password?: string;
32
+ ssl?: boolean;
33
+ file?: string;
34
+ pool?: PoolConfig;
35
+ }
36
+
37
+ export type SqlDriver = SqlConnectionManifest["driver"];
38
+
39
+ export class SqlConnectionResource implements ResourceInstance {
40
+ readonly driver: SqlDriver;
41
+ private readonly db: Kysely<any>;
42
+ private readonly sqlite?: SqliteDb;
43
+
44
+ constructor(m: SqlConnectionManifest, sqlite?: SqliteDb) {
45
+ this.driver = m.driver;
46
+
47
+ if (m.driver === "postgres") {
48
+ this.db = new Kysely({
49
+ dialect: new PostgresDialect({
50
+ pool: new Pool({
51
+ ...(m.connectionString
52
+ ? { connectionString: m.connectionString }
53
+ : { host: m.host, port: m.port ?? 5432, database: m.database, user: m.user, password: m.password }),
54
+ ssl: m.ssl ? { rejectUnauthorized: false } : false,
55
+ min: m.pool?.min ?? 1,
56
+ max: m.pool?.max ?? 10,
57
+ idleTimeoutMillis: m.pool?.idleTimeoutMs,
58
+ connectionTimeoutMillis: m.pool?.connectionTimeoutMs,
59
+ }),
60
+ }),
61
+ });
62
+ } else if (m.driver === "sqlite") {
63
+ if (!sqlite) {
64
+ throw new Error("Sql: sqlite database was not initialized");
65
+ }
66
+ this.sqlite = sqlite;
67
+ this.db = new Kysely({
68
+ dialect: new SqliteDialect({
69
+ database: this.sqlite,
70
+ }),
71
+ });
72
+ } else {
73
+ throw new Error("Invalid SQL Connection driver");
74
+ }
75
+ }
76
+
77
+ async init() {
78
+ await this.db.connection().execute(async () => {
79
+ // just checking
80
+ });
81
+ }
82
+
83
+ async teardown(): Promise<void> {
84
+ await this.db.destroy();
85
+ }
86
+
87
+ async transaction<T>(cb: () => Promise<T>): Promise<T> {
88
+ const txId = randomUUID();
89
+
90
+ return this.db.transaction().execute(async (trx: Transaction<any>) => {
91
+ setTx(txId, { executor: trx });
92
+ try {
93
+ return await txStorage.run(txId, cb);
94
+ } finally {
95
+ deleteTx(txId);
96
+ }
97
+ });
98
+ }
99
+
100
+ async execute<T>(
101
+ sql: string,
102
+ params: unknown[] = [],
103
+ transaction?: SqlTransactionResource,
104
+ ): Promise<QueryResult<T>> {
105
+ const executor = this.resolveExecutor(transaction);
106
+ return executor.executeQuery<T>(CompiledQuery.raw(sql, params));
107
+ }
108
+
109
+ async executeScript(sql: string): Promise<void> {
110
+ if (this.driver === "sqlite") {
111
+ this.sqlite?.exec(sql);
112
+ return;
113
+ }
114
+
115
+ await this.execute(sql);
116
+ }
117
+
118
+ toRowCount(result: QueryResult<unknown>): number {
119
+ if (result.numAffectedRows !== undefined) {
120
+ return Number(result.numAffectedRows);
121
+ }
122
+
123
+ return result.rows.length;
124
+ }
125
+
126
+ get kysely(): Kysely<any> {
127
+ return this.db;
128
+ }
129
+
130
+ snapshot(): Record<string, unknown> {
131
+ return {};
132
+ }
133
+
134
+ private resolveExecutor(transaction?: SqlTransactionResource): Kysely<any> {
135
+ if (transaction) {
136
+ transaction.assertActive();
137
+ }
138
+
139
+ const txId = currentTxId();
140
+ if (txId) {
141
+ const entry = getTx(txId);
142
+ if (entry) {
143
+ return entry.executor as Kysely<any>;
144
+ }
145
+ }
146
+
147
+ return this.db;
148
+ }
149
+ }
150
+
151
+ export function register(): void {}
152
+
153
+ export async function create(
154
+ resource: SqlConnectionManifest,
155
+ ctx: ResourceContext,
156
+ ): Promise<SqlConnectionResource> {
157
+ const sqlite = resource.driver === "sqlite" ? await openSqliteDatabase(resource.file) : undefined;
158
+ return new SqlConnectionResource(resource, sqlite);
159
+ }
160
+
161
+ async function openSqliteDatabase(file = ":memory:"): Promise<SqliteDb> {
162
+ if (typeof Bun !== "undefined") {
163
+ const { openDatabase } = await import("./sqlite-driver-bun.js");
164
+ return openDatabase(file);
165
+ }
166
+
167
+ const { openDatabase } = await import("./sqlite-driver-node.js");
168
+ return openDatabase(file);
169
+ }
@@ -0,0 +1,25 @@
1
+ import type { ResourceContext } from "@telorun/sdk";
2
+ import type { SqlConnectionResource } from "./sql-connection-controller.js";
3
+
4
+ interface ConnectionRef {
5
+ name: string;
6
+ }
7
+
8
+ export function resolveSqlConnection(
9
+ value: SqlConnectionResource | ConnectionRef | undefined,
10
+ ctx: ResourceContext,
11
+ ): SqlConnectionResource | undefined {
12
+ if (!value) {
13
+ return undefined;
14
+ }
15
+
16
+ if (typeof (value as SqlConnectionResource).execute === "function") {
17
+ return value as SqlConnectionResource;
18
+ }
19
+
20
+ if (typeof (value as ConnectionRef).name !== "string") {
21
+ throw new Error("Sql: invalid connection reference");
22
+ }
23
+
24
+ return ctx.moduleContext.getInstance((value as ConnectionRef).name) as SqlConnectionResource;
25
+ }
@@ -0,0 +1,54 @@
1
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import type { SqlConnectionResource } from "./sql-connection-controller.js";
3
+ import { resolveSqlConnection } from "./sql-connection-ref.js";
4
+ import type { SqlResult } from "./sql-query-controller.js";
5
+ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
6
+
7
+ interface SqlExecManifest {
8
+ metadata: { name: string; module: string };
9
+ connection?: SqlConnectionResource;
10
+ transaction?: SqlTransactionResource;
11
+ inputs: {
12
+ sql: string;
13
+ bindings?: unknown[];
14
+ };
15
+ }
16
+
17
+ class SqlExecResource implements ResourceInstance {
18
+ constructor(
19
+ private readonly manifest: SqlExecManifest,
20
+ private readonly ctx: ResourceContext,
21
+ ) {}
22
+
23
+ async invoke(input: any): Promise<SqlResult> {
24
+ const m = this.manifest;
25
+ const ctx = this.ctx;
26
+ const expandedInput = ctx.expandValue(input, {});
27
+
28
+ const connection = resolveSqlConnection(m.connection, ctx) ?? m.transaction?.getConnection();
29
+ if (!connection) {
30
+ throw new Error("Sql: either 'connection' or 'transaction' must be set");
31
+ }
32
+
33
+ return runExec(connection, m.transaction, expandedInput.sql, expandedInput.bindings ?? []);
34
+ }
35
+ }
36
+
37
+ async function runExec(
38
+ connection: SqlConnectionResource,
39
+ transaction: SqlTransactionResource | undefined,
40
+ sql: string,
41
+ params: unknown[],
42
+ ): Promise<SqlResult> {
43
+ const result = await connection.execute<Record<string, unknown>>(sql, params, transaction);
44
+ return { rows: result.rows, rowCount: connection.toRowCount(result) };
45
+ }
46
+
47
+ export function register(): void {}
48
+
49
+ export async function create(
50
+ resource: SqlExecManifest,
51
+ ctx: ResourceContext,
52
+ ): Promise<SqlExecResource> {
53
+ return new SqlExecResource(resource, ctx);
54
+ }
@@ -0,0 +1,20 @@
1
+ import type { ResourceInstance } from "@telorun/sdk";
2
+
3
+ interface SqlMigrationManifest {
4
+ metadata: { name: string; module: string };
5
+ sql: string;
6
+ }
7
+
8
+ class SqlMigrationResource implements ResourceInstance {
9
+ constructor(readonly manifest: SqlMigrationManifest) {}
10
+
11
+ snapshot(): Record<string, unknown> {
12
+ return {};
13
+ }
14
+ }
15
+
16
+ export function register(): void {}
17
+
18
+ export async function create(resource: SqlMigrationManifest): Promise<SqlMigrationResource> {
19
+ return new SqlMigrationResource(resource);
20
+ }
@@ -0,0 +1,85 @@
1
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import {
3
+ CompiledQuery,
4
+ Migrator,
5
+ type Kysely,
6
+ type Migration,
7
+ type MigrationProvider,
8
+ } from "kysely";
9
+ import type { SqlConnectionResource } from "./sql-connection-controller.js";
10
+ import { resolveSqlConnection } from "./sql-connection-ref.js";
11
+
12
+ interface SqlMigrationsManifest {
13
+ metadata: { name: string; module: string };
14
+ connection: SqlConnectionResource;
15
+ }
16
+
17
+ interface MigrationEntry {
18
+ name: string;
19
+ sql: string;
20
+ }
21
+
22
+ class TeloMigrationProvider implements MigrationProvider {
23
+ constructor(private readonly migrations: MigrationEntry[]) {}
24
+
25
+ async getMigrations(): Promise<Record<string, Migration>> {
26
+ return Object.fromEntries(
27
+ this.migrations.map((m) => [
28
+ m.name,
29
+ {
30
+ async up(db: Kysely<any>): Promise<void> {
31
+ await db.executeQuery(CompiledQuery.raw(m.sql));
32
+ },
33
+ },
34
+ ]),
35
+ );
36
+ }
37
+ }
38
+
39
+ class SqlMigrationsResource implements ResourceInstance {
40
+ constructor(
41
+ private readonly manifest: SqlMigrationsManifest,
42
+ private readonly ctx: ResourceContext,
43
+ ) {}
44
+
45
+ async run(): Promise<void> {
46
+ const conn =
47
+ resolveSqlConnection(this.manifest.connection, this.ctx) ?? failMissingConnection();
48
+
49
+ const migrations: MigrationEntry[] = [];
50
+ for (const [, { resource }] of this.ctx.moduleContext.resourceInstances) {
51
+ if (resource.kind === "Sql.Migration") {
52
+ migrations.push({
53
+ name: resource.metadata.name as string,
54
+ sql: resource.sql as string,
55
+ });
56
+ }
57
+ }
58
+ migrations.sort((a, b) => a.name.localeCompare(b.name));
59
+
60
+ const migrator = new Migrator({
61
+ db: conn.kysely,
62
+ provider: new TeloMigrationProvider(migrations),
63
+ migrationTableName: "migrations",
64
+ migrationLockTableName: "migration_locks",
65
+ });
66
+
67
+ const { error } = await migrator.migrateToLatest();
68
+ if (error) {
69
+ throw error;
70
+ }
71
+ }
72
+ }
73
+
74
+ function failMissingConnection(): never {
75
+ throw new Error("Sql.Migrations: missing connection");
76
+ }
77
+
78
+ export function register(): void {}
79
+
80
+ export async function create(
81
+ resource: SqlMigrationsManifest,
82
+ ctx: ResourceContext,
83
+ ): Promise<SqlMigrationsResource> {
84
+ return new SqlMigrationsResource(resource, ctx);
85
+ }
@@ -0,0 +1,68 @@
1
+ import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
+ import type { SqlConnectionResource } from "./sql-connection-controller.js";
3
+ import { resolveSqlConnection } from "./sql-connection-ref.js";
4
+ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
5
+
6
+ interface SqlQueryManifest {
7
+ metadata: { name: string; module: string };
8
+ connection?: SqlConnectionResource;
9
+ transaction?: SqlTransactionResource;
10
+ inputs: {
11
+ sql: string;
12
+ bindings?: unknown[];
13
+ };
14
+ }
15
+
16
+ export interface SqlResult {
17
+ rows: Record<string, unknown>[];
18
+ rowCount: number;
19
+ }
20
+
21
+ class SqlQueryResource implements ResourceInstance {
22
+ constructor(
23
+ private readonly manifest: SqlQueryManifest,
24
+ private readonly ctx: ResourceContext,
25
+ ) {}
26
+
27
+ async invoke(input: unknown): Promise<SqlResult> {
28
+ const m = this.manifest;
29
+ const ctx = this.ctx;
30
+ const expandedInput = ctx.expandValue(input, {});
31
+
32
+ const connection = resolveConnection(m.connection, m.transaction, ctx);
33
+ return runQuery(connection, m.transaction, expandedInput.sql, expandedInput.bindings);
34
+ }
35
+ }
36
+
37
+ function resolveConnection(
38
+ connection: SqlConnectionResource | undefined,
39
+ transaction: SqlTransactionResource | undefined,
40
+ ctx: ResourceContext,
41
+ ): SqlConnectionResource {
42
+ return (
43
+ resolveSqlConnection(connection, ctx) ?? transaction?.getConnection() ?? failMissingConnection()
44
+ );
45
+ }
46
+
47
+ async function runQuery(
48
+ connection: SqlConnectionResource,
49
+ transaction: SqlTransactionResource | undefined,
50
+ sql: string,
51
+ params: unknown[],
52
+ ): Promise<SqlResult> {
53
+ const result = await connection.execute<Record<string, unknown>>(sql, params, transaction);
54
+ return { rows: result.rows, rowCount: result.rows.length };
55
+ }
56
+
57
+ function failMissingConnection(): never {
58
+ throw new Error("Sql: either 'connection' or 'transaction' must be set");
59
+ }
60
+
61
+ export function register(): void {}
62
+
63
+ export async function create(
64
+ resource: SqlQueryManifest,
65
+ ctx: ResourceContext,
66
+ ): Promise<SqlQueryResource> {
67
+ return new SqlQueryResource(resource, ctx);
68
+ }