@telorun/sql 0.3.1 → 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.
@@ -13,18 +13,10 @@ interface SqlConnectionManifest {
13
13
  name: string;
14
14
  module: string;
15
15
  };
16
- driver: "postgres" | "sqlite";
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 = SqlConnectionManifest["driver"];
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.driver;
11
- if (m.driver === "postgres") {
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
- ...(m.connectionString
16
- ? { connectionString: m.connectionString }
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 if (m.driver === "sqlite") {
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.driver === "sqlite" ? await openSqliteDatabase(resource.file) : undefined;
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:` and `file::memory:?...`
109
- // skip filesystem entirely.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/sql",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -22,19 +22,11 @@ interface PoolConfig {
22
22
 
23
23
  interface SqlConnectionManifest {
24
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;
25
+ connectionString: string;
34
26
  pool?: PoolConfig;
35
27
  }
36
28
 
37
- export type SqlDriver = SqlConnectionManifest["driver"];
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.driver;
37
+ this.driver = driverFromConnectionString(m.connectionString);
46
38
 
47
- if (m.driver === "postgres") {
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
- ...(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,
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 if (m.driver === "sqlite") {
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 = resource.driver === "sqlite" ? await openSqliteDatabase(resource.file) : undefined;
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:` and `file::memory:?...`
166
- // skip filesystem entirely.
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);