@telorun/sql 0.5.1 → 0.6.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 CHANGED
@@ -54,20 +54,13 @@ metadata:
54
54
  description: |
55
55
  A complete feedback collection REST API — no code, pure YAML.
56
56
  Persists entries to SQLite and serves them over HTTP.
57
+ imports:
58
+ Http: std/http-server@0.8.0
59
+ Sql: std/sql@0.5.1
57
60
  targets:
58
61
  - Migrations
59
62
  - Server
60
63
  ---
61
- kind: Telo.Import
62
- metadata:
63
- name: Http
64
- source: std/http-server@0.5.0
65
- ---
66
- kind: Telo.Import
67
- metadata:
68
- name: Sql
69
- source: std/sql@0.3.0
70
- ---
71
64
  # SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
72
65
  kind: Sql.Connection
73
66
  metadata:
@@ -127,7 +120,7 @@ routes:
127
120
  minLength: 1
128
121
  source:
129
122
  type: string
130
- required: [text]
123
+ required: [ text ]
131
124
  handler:
132
125
  kind: Sql.Exec
133
126
  connection:
@@ -157,7 +150,7 @@ routes:
157
150
  kind: Sql.Connection
158
151
  name: Db
159
152
  from: feedback
160
- columns: [id, text, source, score, created_at]
153
+ columns: [ id, text, source, score, created_at ]
161
154
  orderBy:
162
155
  - { column: created_at, direction: desc }
163
156
  response:
@@ -176,14 +169,14 @@ routes:
176
169
  properties:
177
170
  id:
178
171
  type: integer
179
- required: [id]
172
+ required: [ id ]
180
173
  handler:
181
174
  kind: Sql.Select
182
175
  connection:
183
176
  kind: Sql.Connection
184
177
  name: Db
185
178
  from: feedback
186
- columns: [id, text, source, score, created_at]
179
+ columns: [ id, text, source, score, created_at ]
187
180
  where:
188
181
  - { column: id, op: "=", value: "${{ request.params.id }}" }
189
182
  response:
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "crypto";
2
- import { CompiledQuery, Kysely, PostgresDialect, SqliteDialect, } from "kysely";
2
+ import { CompiledQuery, Kysely, PostgresDialect, SqliteAdapter, SqliteDialect, } from "kysely";
3
3
  import { Pool } from "pg";
4
4
  import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-store.js";
5
5
  export class SqlConnectionResource {
@@ -31,7 +31,7 @@ export class SqlConnectionResource {
31
31
  }
32
32
  this.sqlite = sqlite;
33
33
  this.db = new Kysely({
34
- dialect: new SqliteDialect({
34
+ dialect: new TransactionalSqliteDialect({
35
35
  database: this.sqlite,
36
36
  }),
37
37
  });
@@ -94,6 +94,20 @@ export class SqlConnectionResource {
94
94
  return this.db;
95
95
  }
96
96
  }
97
+ // Kysely's stock SQLite adapter reports `supportsTransactionalDdl = false`, so
98
+ // its Migrator runs migrations without a transaction. SQLite does support
99
+ // transactional DDL, so we flip the flag — letting the Migrator wrap the whole
100
+ // migration batch in a single transaction, matching PostgreSQL.
101
+ class TransactionalSqliteAdapter extends SqliteAdapter {
102
+ get supportsTransactionalDdl() {
103
+ return true;
104
+ }
105
+ }
106
+ class TransactionalSqliteDialect extends SqliteDialect {
107
+ createAdapter() {
108
+ return new TransactionalSqliteAdapter();
109
+ }
110
+ }
97
111
  export function register() { }
98
112
  export async function create(resource, ctx) {
99
113
  const sqlite = driverFromConnectionString(resource.connectionString) === "sqlite"
@@ -1,11 +1,16 @@
1
1
  import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
2
  import type { SqlConnectionResource } from "./sql-connection-controller.js";
3
+ interface MigrationEntry {
4
+ statement?: string;
5
+ statements?: string[];
6
+ }
3
7
  interface SqlMigrationsManifest {
4
8
  metadata: {
5
9
  name: string;
6
10
  module: string;
7
11
  };
8
12
  connection: SqlConnectionResource;
13
+ migrations?: Record<string, MigrationEntry>;
9
14
  }
10
15
  declare class SqlMigrationsResource implements ResourceInstance {
11
16
  private readonly manifest;
@@ -1,16 +1,24 @@
1
1
  import { CompiledQuery, Migrator, } from "kysely";
2
2
  import { resolveSqlConnection } from "./sql-connection-ref.js";
3
+ function entryStatements(entry) {
4
+ return entry.statements ?? (entry.statement != null ? [entry.statement] : []);
5
+ }
3
6
  class TeloMigrationProvider {
4
7
  migrations;
5
8
  constructor(migrations) {
6
9
  this.migrations = migrations;
7
10
  }
8
11
  async getMigrations() {
9
- return Object.fromEntries(this.migrations.map((m) => [
10
- m.name,
12
+ return Object.fromEntries(Object.entries(this.migrations).map(([name, statements]) => [
13
+ name,
11
14
  {
15
+ // Each statement runs as its own prepared statement on the migration
16
+ // transaction's connection, so a migration may hold multiple
17
+ // statements while the whole batch stays a single transaction.
12
18
  async up(db) {
13
- await db.executeQuery(CompiledQuery.raw(m.sql));
19
+ for (const statement of statements) {
20
+ await db.executeQuery(CompiledQuery.raw(statement));
21
+ }
14
22
  },
15
23
  },
16
24
  ]));
@@ -25,17 +33,23 @@ class SqlMigrationsResource {
25
33
  }
26
34
  async run() {
27
35
  const conn = resolveSqlConnection(this.manifest.connection, this.ctx) ?? failMissingConnection();
28
- const migrations = [];
36
+ const migrations = {};
37
+ // Legacy: standalone `Sql.Migration` resources in the same module scope.
29
38
  for (const [, { resource }] of this.ctx.moduleContext.resourceInstances) {
30
39
  if (resource.kind === "Sql.Migration") {
31
40
  const version = (resource.version ?? resource.metadata.name);
32
- migrations.push({
33
- name: version,
34
- sql: resource.sql,
35
- });
41
+ migrations[version] = [resource.sql];
42
+ }
43
+ }
44
+ // Preferred: the keyed `migrations` map on this resource.
45
+ for (const [name, entry] of Object.entries(this.manifest.migrations ?? {})) {
46
+ const statements = entryStatements(entry);
47
+ if (statements.length === 0) {
48
+ throw new Error(`Sql.Migrations: migration '${name}' has no statement(s) — ` +
49
+ `set 'statement' or a non-empty 'statements'`);
36
50
  }
51
+ migrations[name] = statements;
37
52
  }
38
- migrations.sort((a, b) => a.name.localeCompare(b.name));
39
53
  const migrator = new Migrator({
40
54
  db: conn.kysely,
41
55
  provider: new TeloMigrationProvider(migrations),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/sql",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -70,7 +70,7 @@
70
70
  "@types/node": "^20.0.0",
71
71
  "@types/pg": "^8.0.0",
72
72
  "typescript": "^5.0.0",
73
- "@telorun/sdk": "0.16.0"
73
+ "@telorun/sdk": "0.19.0"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@telorun/sdk": "*"
@@ -4,6 +4,7 @@ import {
4
4
  CompiledQuery,
5
5
  Kysely,
6
6
  PostgresDialect,
7
+ SqliteAdapter,
7
8
  SqliteDialect,
8
9
  type QueryResult,
9
10
  type Transaction,
@@ -58,7 +59,7 @@ export class SqlConnectionResource implements ResourceInstance {
58
59
  }
59
60
  this.sqlite = sqlite;
60
61
  this.db = new Kysely({
61
- dialect: new SqliteDialect({
62
+ dialect: new TransactionalSqliteDialect({
62
63
  database: this.sqlite,
63
64
  }),
64
65
  });
@@ -139,6 +140,22 @@ export class SqlConnectionResource implements ResourceInstance {
139
140
  }
140
141
  }
141
142
 
143
+ // Kysely's stock SQLite adapter reports `supportsTransactionalDdl = false`, so
144
+ // its Migrator runs migrations without a transaction. SQLite does support
145
+ // transactional DDL, so we flip the flag — letting the Migrator wrap the whole
146
+ // migration batch in a single transaction, matching PostgreSQL.
147
+ class TransactionalSqliteAdapter extends SqliteAdapter {
148
+ override get supportsTransactionalDdl(): boolean {
149
+ return true;
150
+ }
151
+ }
152
+
153
+ class TransactionalSqliteDialect extends SqliteDialect {
154
+ override createAdapter(): SqliteAdapter {
155
+ return new TransactionalSqliteAdapter();
156
+ }
157
+ }
158
+
142
159
  export function register(): void {}
143
160
 
144
161
  export async function create(
@@ -9,26 +9,38 @@ import {
9
9
  import type { SqlConnectionResource } from "./sql-connection-controller.js";
10
10
  import { resolveSqlConnection } from "./sql-connection-ref.js";
11
11
 
12
+ // A migration entry is one statement or an ordered list of statements; both
13
+ // forms normalize to a non-empty array of single statements.
14
+ interface MigrationEntry {
15
+ statement?: string;
16
+ statements?: string[];
17
+ }
18
+
12
19
  interface SqlMigrationsManifest {
13
20
  metadata: { name: string; module: string };
14
21
  connection: SqlConnectionResource;
22
+ migrations?: Record<string, MigrationEntry>;
15
23
  }
16
24
 
17
- interface MigrationEntry {
18
- name: string;
19
- sql: string;
25
+ function entryStatements(entry: MigrationEntry): string[] {
26
+ return entry.statements ?? (entry.statement != null ? [entry.statement] : []);
20
27
  }
21
28
 
22
29
  class TeloMigrationProvider implements MigrationProvider {
23
- constructor(private readonly migrations: MigrationEntry[]) {}
30
+ constructor(private readonly migrations: Record<string, string[]>) {}
24
31
 
25
32
  async getMigrations(): Promise<Record<string, Migration>> {
26
33
  return Object.fromEntries(
27
- this.migrations.map((m) => [
28
- m.name,
34
+ Object.entries(this.migrations).map(([name, statements]) => [
35
+ name,
29
36
  {
37
+ // Each statement runs as its own prepared statement on the migration
38
+ // transaction's connection, so a migration may hold multiple
39
+ // statements while the whole batch stays a single transaction.
30
40
  async up(db: Kysely<any>): Promise<void> {
31
- await db.executeQuery(CompiledQuery.raw(m.sql));
41
+ for (const statement of statements) {
42
+ await db.executeQuery(CompiledQuery.raw(statement));
43
+ }
32
44
  },
33
45
  },
34
46
  ]),
@@ -46,17 +58,25 @@ class SqlMigrationsResource implements ResourceInstance {
46
58
  const conn =
47
59
  resolveSqlConnection(this.manifest.connection, this.ctx) ?? failMissingConnection();
48
60
 
49
- const migrations: MigrationEntry[] = [];
61
+ const migrations: Record<string, string[]> = {};
62
+ // Legacy: standalone `Sql.Migration` resources in the same module scope.
50
63
  for (const [, { resource }] of this.ctx.moduleContext.resourceInstances) {
51
64
  if (resource.kind === "Sql.Migration") {
52
65
  const version = (resource.version ?? resource.metadata.name) as string;
53
- migrations.push({
54
- name: version,
55
- sql: resource.sql as string,
56
- });
66
+ migrations[version] = [resource.sql as string];
67
+ }
68
+ }
69
+ // Preferred: the keyed `migrations` map on this resource.
70
+ for (const [name, entry] of Object.entries(this.manifest.migrations ?? {})) {
71
+ const statements = entryStatements(entry);
72
+ if (statements.length === 0) {
73
+ throw new Error(
74
+ `Sql.Migrations: migration '${name}' has no statement(s) — ` +
75
+ `set 'statement' or a non-empty 'statements'`,
76
+ );
57
77
  }
78
+ migrations[name] = statements;
58
79
  }
59
- migrations.sort((a, b) => a.name.localeCompare(b.name));
60
80
 
61
81
  const migrator = new Migrator({
62
82
  db: conn.kysely,