@telorun/sql 0.3.0 → 0.4.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
|
@@ -61,12 +61,12 @@ targets:
|
|
|
61
61
|
kind: Telo.Import
|
|
62
62
|
metadata:
|
|
63
63
|
name: Http
|
|
64
|
-
source: std/http-server@0.
|
|
64
|
+
source: std/http-server@0.5.0
|
|
65
65
|
---
|
|
66
66
|
kind: Telo.Import
|
|
67
67
|
metadata:
|
|
68
68
|
name: Sql
|
|
69
|
-
source: std/sql@0.
|
|
69
|
+
source: std/sql@0.3.0
|
|
70
70
|
---
|
|
71
71
|
# SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
|
|
72
72
|
kind: Sql.Connection
|
|
@@ -13,18 +13,10 @@ interface SqlConnectionManifest {
|
|
|
13
13
|
name: string;
|
|
14
14
|
module: string;
|
|
15
15
|
};
|
|
16
|
-
|
|
17
|
-
connectionString?: string;
|
|
18
|
-
host?: string;
|
|
19
|
-
port?: number;
|
|
20
|
-
database?: string;
|
|
21
|
-
user?: string;
|
|
22
|
-
password?: string;
|
|
23
|
-
ssl?: boolean;
|
|
24
|
-
file?: string;
|
|
16
|
+
connectionString: string;
|
|
25
17
|
pool?: PoolConfig;
|
|
26
18
|
}
|
|
27
|
-
export type SqlDriver =
|
|
19
|
+
export type SqlDriver = "postgres" | "sqlite";
|
|
28
20
|
export declare class SqlConnectionResource implements ResourceInstance {
|
|
29
21
|
readonly driver: SqlDriver;
|
|
30
22
|
private readonly db;
|
|
@@ -7,15 +7,16 @@ export class SqlConnectionResource {
|
|
|
7
7
|
db;
|
|
8
8
|
sqlite;
|
|
9
9
|
constructor(m, sqlite) {
|
|
10
|
-
this.driver = m.
|
|
11
|
-
if (
|
|
10
|
+
this.driver = driverFromConnectionString(m.connectionString);
|
|
11
|
+
if (this.driver === "postgres") {
|
|
12
|
+
const url = new URL(m.connectionString);
|
|
13
|
+
const ssl = sslFromSslmode(url.searchParams.get("sslmode"));
|
|
14
|
+
url.searchParams.delete("sslmode");
|
|
12
15
|
this.db = new Kysely({
|
|
13
16
|
dialect: new PostgresDialect({
|
|
14
17
|
pool: new Pool({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
: { host: m.host, port: m.port ?? 5432, database: m.database, user: m.user, password: m.password }),
|
|
18
|
-
ssl: m.ssl ? { rejectUnauthorized: false } : false,
|
|
18
|
+
connectionString: url.toString(),
|
|
19
|
+
ssl,
|
|
19
20
|
min: m.pool?.min ?? 1,
|
|
20
21
|
max: m.pool?.max ?? 10,
|
|
21
22
|
idleTimeoutMillis: m.pool?.idleTimeoutMs,
|
|
@@ -24,7 +25,7 @@ export class SqlConnectionResource {
|
|
|
24
25
|
}),
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
|
-
else
|
|
28
|
+
else {
|
|
28
29
|
if (!sqlite) {
|
|
29
30
|
throw new Error("Sql: sqlite database was not initialized");
|
|
30
31
|
}
|
|
@@ -35,9 +36,6 @@ export class SqlConnectionResource {
|
|
|
35
36
|
}),
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
|
-
else {
|
|
39
|
-
throw new Error("Invalid SQL Connection driver");
|
|
40
|
-
}
|
|
41
39
|
}
|
|
42
40
|
async init() {
|
|
43
41
|
await this.db.connection().execute(async () => {
|
|
@@ -98,16 +96,54 @@ export class SqlConnectionResource {
|
|
|
98
96
|
}
|
|
99
97
|
export function register() { }
|
|
100
98
|
export async function create(resource, ctx) {
|
|
101
|
-
const sqlite = resource.
|
|
99
|
+
const sqlite = driverFromConnectionString(resource.connectionString) === "sqlite"
|
|
100
|
+
? await openSqliteDatabase(sqliteTargetFromConnectionString(resource.connectionString))
|
|
101
|
+
: undefined;
|
|
102
102
|
return new SqlConnectionResource(resource, sqlite);
|
|
103
103
|
}
|
|
104
|
+
function driverFromConnectionString(connectionString) {
|
|
105
|
+
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(connectionString)?.[1]?.toLowerCase();
|
|
106
|
+
switch (scheme) {
|
|
107
|
+
case "postgres":
|
|
108
|
+
case "postgresql":
|
|
109
|
+
return "postgres";
|
|
110
|
+
case "sqlite":
|
|
111
|
+
return "sqlite";
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(`Sql.Connection: connectionString must start with a driver scheme — ` +
|
|
114
|
+
`'postgres://' or 'postgresql://' for PostgreSQL, 'sqlite:' for SQLite. ` +
|
|
115
|
+
`Got ${scheme ? `'${scheme}:'` : "a string with no scheme"}: ${JSON.stringify(connectionString)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function sqliteTargetFromConnectionString(connectionString) {
|
|
119
|
+
const path = decodeURIComponent(new URL(connectionString).pathname);
|
|
120
|
+
// `sqlite:` / `sqlite://` with no path resolves to an in-memory database.
|
|
121
|
+
return path === "" || path === "/" ? ":memory:" : path;
|
|
122
|
+
}
|
|
123
|
+
function sslFromSslmode(mode) {
|
|
124
|
+
switch (mode) {
|
|
125
|
+
case null:
|
|
126
|
+
case "disable":
|
|
127
|
+
return false;
|
|
128
|
+
case "require":
|
|
129
|
+
return { rejectUnauthorized: false };
|
|
130
|
+
case "verify-ca":
|
|
131
|
+
// libpq `verify-ca` validates the CA chain but not the hostname; Node's
|
|
132
|
+
// default `checkServerIdentity` enforces the hostname, so disable it.
|
|
133
|
+
return { rejectUnauthorized: true, checkServerIdentity: () => undefined };
|
|
134
|
+
case "verify-full":
|
|
135
|
+
return { rejectUnauthorized: true };
|
|
136
|
+
default:
|
|
137
|
+
throw new Error(`Sql.Connection: unsupported sslmode '${mode}'. ` +
|
|
138
|
+
`Use 'disable', 'require', 'verify-ca', or 'verify-full'.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
104
141
|
async function openSqliteDatabase(file = ":memory:") {
|
|
105
142
|
// Auto-create the parent directory for file-backed databases. SQLite
|
|
106
143
|
// drivers fail-fast when the directory doesn't exist; mirroring `mkdir
|
|
107
144
|
// -p` here lets manifests use paths like `./tmp/foo.sqlite` without a
|
|
108
|
-
// separate filesystem-prep step. `:memory:`
|
|
109
|
-
|
|
110
|
-
if (file !== ":memory:" && !file.startsWith("file::memory:")) {
|
|
145
|
+
// separate filesystem-prep step. `:memory:` skips filesystem entirely.
|
|
146
|
+
if (file !== ":memory:") {
|
|
111
147
|
const { mkdir } = await import("node:fs/promises");
|
|
112
148
|
const { dirname } = await import("node:path");
|
|
113
149
|
const dir = dirname(file);
|
package/package.json
CHANGED
|
@@ -22,19 +22,11 @@ interface PoolConfig {
|
|
|
22
22
|
|
|
23
23
|
interface SqlConnectionManifest {
|
|
24
24
|
metadata: { name: string; module: string };
|
|
25
|
-
|
|
26
|
-
connectionString?: string;
|
|
27
|
-
host?: string;
|
|
28
|
-
port?: number;
|
|
29
|
-
database?: string;
|
|
30
|
-
user?: string;
|
|
31
|
-
password?: string;
|
|
32
|
-
ssl?: boolean;
|
|
33
|
-
file?: string;
|
|
25
|
+
connectionString: string;
|
|
34
26
|
pool?: PoolConfig;
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
export type SqlDriver =
|
|
29
|
+
export type SqlDriver = "postgres" | "sqlite";
|
|
38
30
|
|
|
39
31
|
export class SqlConnectionResource implements ResourceInstance {
|
|
40
32
|
readonly driver: SqlDriver;
|
|
@@ -42,16 +34,17 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
42
34
|
private readonly sqlite?: SqliteDb;
|
|
43
35
|
|
|
44
36
|
constructor(m: SqlConnectionManifest, sqlite?: SqliteDb) {
|
|
45
|
-
this.driver = m.
|
|
37
|
+
this.driver = driverFromConnectionString(m.connectionString);
|
|
46
38
|
|
|
47
|
-
if (
|
|
39
|
+
if (this.driver === "postgres") {
|
|
40
|
+
const url = new URL(m.connectionString);
|
|
41
|
+
const ssl = sslFromSslmode(url.searchParams.get("sslmode"));
|
|
42
|
+
url.searchParams.delete("sslmode");
|
|
48
43
|
this.db = new Kysely({
|
|
49
44
|
dialect: new PostgresDialect({
|
|
50
45
|
pool: new Pool({
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
: { host: m.host, port: m.port ?? 5432, database: m.database, user: m.user, password: m.password }),
|
|
54
|
-
ssl: m.ssl ? { rejectUnauthorized: false } : false,
|
|
46
|
+
connectionString: url.toString(),
|
|
47
|
+
ssl,
|
|
55
48
|
min: m.pool?.min ?? 1,
|
|
56
49
|
max: m.pool?.max ?? 10,
|
|
57
50
|
idleTimeoutMillis: m.pool?.idleTimeoutMs,
|
|
@@ -59,7 +52,7 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
59
52
|
}),
|
|
60
53
|
}),
|
|
61
54
|
});
|
|
62
|
-
} else
|
|
55
|
+
} else {
|
|
63
56
|
if (!sqlite) {
|
|
64
57
|
throw new Error("Sql: sqlite database was not initialized");
|
|
65
58
|
}
|
|
@@ -69,8 +62,6 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
69
62
|
database: this.sqlite,
|
|
70
63
|
}),
|
|
71
64
|
});
|
|
72
|
-
} else {
|
|
73
|
-
throw new Error("Invalid SQL Connection driver");
|
|
74
65
|
}
|
|
75
66
|
}
|
|
76
67
|
|
|
@@ -154,17 +145,67 @@ export async function create(
|
|
|
154
145
|
resource: SqlConnectionManifest,
|
|
155
146
|
ctx: ResourceContext,
|
|
156
147
|
): Promise<SqlConnectionResource> {
|
|
157
|
-
const sqlite =
|
|
148
|
+
const sqlite =
|
|
149
|
+
driverFromConnectionString(resource.connectionString) === "sqlite"
|
|
150
|
+
? await openSqliteDatabase(sqliteTargetFromConnectionString(resource.connectionString))
|
|
151
|
+
: undefined;
|
|
158
152
|
return new SqlConnectionResource(resource, sqlite);
|
|
159
153
|
}
|
|
160
154
|
|
|
155
|
+
type SslOption =
|
|
156
|
+
| false
|
|
157
|
+
| { rejectUnauthorized: boolean; checkServerIdentity?: () => undefined };
|
|
158
|
+
|
|
159
|
+
function driverFromConnectionString(connectionString: string): SqlDriver {
|
|
160
|
+
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(connectionString)?.[1]?.toLowerCase();
|
|
161
|
+
switch (scheme) {
|
|
162
|
+
case "postgres":
|
|
163
|
+
case "postgresql":
|
|
164
|
+
return "postgres";
|
|
165
|
+
case "sqlite":
|
|
166
|
+
return "sqlite";
|
|
167
|
+
default:
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Sql.Connection: connectionString must start with a driver scheme — ` +
|
|
170
|
+
`'postgres://' or 'postgresql://' for PostgreSQL, 'sqlite:' for SQLite. ` +
|
|
171
|
+
`Got ${scheme ? `'${scheme}:'` : "a string with no scheme"}: ${JSON.stringify(connectionString)}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sqliteTargetFromConnectionString(connectionString: string): string {
|
|
177
|
+
const path = decodeURIComponent(new URL(connectionString).pathname);
|
|
178
|
+
// `sqlite:` / `sqlite://` with no path resolves to an in-memory database.
|
|
179
|
+
return path === "" || path === "/" ? ":memory:" : path;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sslFromSslmode(mode: string | null): SslOption {
|
|
183
|
+
switch (mode) {
|
|
184
|
+
case null:
|
|
185
|
+
case "disable":
|
|
186
|
+
return false;
|
|
187
|
+
case "require":
|
|
188
|
+
return { rejectUnauthorized: false };
|
|
189
|
+
case "verify-ca":
|
|
190
|
+
// libpq `verify-ca` validates the CA chain but not the hostname; Node's
|
|
191
|
+
// default `checkServerIdentity` enforces the hostname, so disable it.
|
|
192
|
+
return { rejectUnauthorized: true, checkServerIdentity: () => undefined };
|
|
193
|
+
case "verify-full":
|
|
194
|
+
return { rejectUnauthorized: true };
|
|
195
|
+
default:
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Sql.Connection: unsupported sslmode '${mode}'. ` +
|
|
198
|
+
`Use 'disable', 'require', 'verify-ca', or 'verify-full'.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
161
203
|
async function openSqliteDatabase(file = ":memory:"): Promise<SqliteDb> {
|
|
162
204
|
// Auto-create the parent directory for file-backed databases. SQLite
|
|
163
205
|
// drivers fail-fast when the directory doesn't exist; mirroring `mkdir
|
|
164
206
|
// -p` here lets manifests use paths like `./tmp/foo.sqlite` without a
|
|
165
|
-
// separate filesystem-prep step. `:memory:`
|
|
166
|
-
|
|
167
|
-
if (file !== ":memory:" && !file.startsWith("file::memory:")) {
|
|
207
|
+
// separate filesystem-prep step. `:memory:` skips filesystem entirely.
|
|
208
|
+
if (file !== ":memory:") {
|
|
168
209
|
const { mkdir } = await import("node:fs/promises");
|
|
169
210
|
const { dirname } = await import("node:path");
|
|
170
211
|
const dir = dirname(file);
|