@telorun/sql 0.5.0 → 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
|
|
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((
|
|
10
|
-
|
|
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
|
-
|
|
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.
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -69,10 +69,11 @@
|
|
|
69
69
|
"@types/bun": "^1.3.10",
|
|
70
70
|
"@types/node": "^20.0.0",
|
|
71
71
|
"@types/pg": "^8.0.0",
|
|
72
|
-
"typescript": "^5.0.0"
|
|
72
|
+
"typescript": "^5.0.0",
|
|
73
|
+
"@telorun/sdk": "0.19.0"
|
|
73
74
|
},
|
|
74
75
|
"peerDependencies": {
|
|
75
|
-
"@telorun/sdk": "
|
|
76
|
+
"@telorun/sdk": "*"
|
|
76
77
|
},
|
|
77
78
|
"scripts": {
|
|
78
79
|
"build": "tsc -p tsconfig.lib.json"
|
|
@@ -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
|
|
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
|
-
|
|
18
|
-
|
|
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:
|
|
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((
|
|
28
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
54
|
-
|
|
55
|
-
|
|
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,
|