@vertz/db 0.2.0 → 0.2.1
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 +412 -694
- package/dist/d1/index.d.ts +241 -0
- package/dist/d1/index.js +8 -0
- package/dist/diagnostic/index.js +1 -1
- package/dist/index.d.ts +932 -626
- package/dist/index.js +1753 -533
- package/dist/internals.d.ts +96 -31
- package/dist/internals.js +8 -7
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +7 -3
- package/dist/postgres/index.d.ts +77 -0
- package/dist/postgres/index.js +7 -0
- package/dist/shared/{chunk-3f2grpak.js → chunk-0e1vy9qd.js} +147 -52
- package/dist/shared/chunk-2gd1fqcw.js +7 -0
- package/dist/shared/{chunk-xp022dyp.js → chunk-agyds4jw.js} +25 -19
- package/dist/shared/chunk-dvwe5jsq.js +7 -0
- package/dist/shared/chunk-fwk49jvg.js +302 -0
- package/dist/shared/chunk-j4kwq1gh.js +5 -0
- package/dist/shared/{chunk-wj026daz.js → chunk-k04v1jjx.js} +2 -2
- package/dist/shared/chunk-kb4tnn2k.js +26 -0
- package/dist/shared/chunk-ktbebkz5.js +48 -0
- package/dist/shared/chunk-rqe0prft.js +100 -0
- package/dist/shared/chunk-ssga2xea.js +9 -0
- package/dist/shared/{chunk-hrfdj0rr.js → chunk-v2qm94qp.js} +12 -2
- package/dist/sql/index.d.ts +61 -61
- package/dist/sql/index.js +2 -2
- package/dist/sqlite/index.d.ts +221 -0
- package/dist/sqlite/index.js +845 -0
- package/package.json +31 -4
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// src/id/generators.ts
|
|
2
|
+
import { createId } from "@paralleldrive/cuid2";
|
|
3
|
+
import { v7 as uuidv7 } from "uuid";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
function generateId(strategy) {
|
|
6
|
+
switch (strategy) {
|
|
7
|
+
case "cuid":
|
|
8
|
+
return createId();
|
|
9
|
+
case "uuid":
|
|
10
|
+
return uuidv7();
|
|
11
|
+
case "nanoid":
|
|
12
|
+
return nanoid();
|
|
13
|
+
default:
|
|
14
|
+
throw new Error(`Unknown ID generation strategy: ${strategy}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/adapters/sql-utils.ts
|
|
19
|
+
function getSqlType(meta) {
|
|
20
|
+
switch (meta.sqlType) {
|
|
21
|
+
case "serial":
|
|
22
|
+
return "INTEGER";
|
|
23
|
+
case "varchar":
|
|
24
|
+
return meta.length ? `VARCHAR(${meta.length})` : "TEXT";
|
|
25
|
+
case "text":
|
|
26
|
+
return "TEXT";
|
|
27
|
+
case "integer":
|
|
28
|
+
return "INTEGER";
|
|
29
|
+
case "bigint":
|
|
30
|
+
return "BIGINT";
|
|
31
|
+
case "decimal":
|
|
32
|
+
return meta.precision && meta.scale ? `DECIMAL(${meta.precision},${meta.scale})` : "REAL";
|
|
33
|
+
case "boolean":
|
|
34
|
+
return "INTEGER";
|
|
35
|
+
case "timestamp":
|
|
36
|
+
case "timestamptz":
|
|
37
|
+
return "TEXT";
|
|
38
|
+
case "date":
|
|
39
|
+
return "TEXT";
|
|
40
|
+
case "json":
|
|
41
|
+
case "jsonb":
|
|
42
|
+
return "TEXT";
|
|
43
|
+
case "uuid":
|
|
44
|
+
return "TEXT";
|
|
45
|
+
case "enum":
|
|
46
|
+
return "TEXT";
|
|
47
|
+
default:
|
|
48
|
+
return "TEXT";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function generateCreateTableSql(schema) {
|
|
52
|
+
const columns = [];
|
|
53
|
+
const tableName = schema._name;
|
|
54
|
+
for (const [colName, colBuilder] of Object.entries(schema._columns)) {
|
|
55
|
+
const meta = colBuilder._meta;
|
|
56
|
+
let colDef = `${colName} ${getSqlType(meta)}`;
|
|
57
|
+
if (meta.primary) {
|
|
58
|
+
colDef += " PRIMARY KEY";
|
|
59
|
+
if (meta.generate === "uuid") {
|
|
60
|
+
colDef += " DEFAULT (uuid())";
|
|
61
|
+
} else if (meta.generate === "cuid") {
|
|
62
|
+
colDef += " DEFAULT (cuid())";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (meta.unique && !meta.primary) {
|
|
66
|
+
colDef += " UNIQUE";
|
|
67
|
+
}
|
|
68
|
+
if (!meta.nullable && !meta.primary) {
|
|
69
|
+
colDef += " NOT NULL";
|
|
70
|
+
}
|
|
71
|
+
if (meta.hasDefault && meta.defaultValue !== undefined) {
|
|
72
|
+
if (meta.defaultValue === "now") {
|
|
73
|
+
colDef += " DEFAULT (datetime('now'))";
|
|
74
|
+
} else if (typeof meta.defaultValue === "string") {
|
|
75
|
+
colDef += ` DEFAULT '${meta.defaultValue}'`;
|
|
76
|
+
} else if (typeof meta.defaultValue === "number") {
|
|
77
|
+
colDef += ` DEFAULT ${meta.defaultValue}`;
|
|
78
|
+
} else if (typeof meta.defaultValue === "boolean") {
|
|
79
|
+
colDef += ` DEFAULT ${meta.defaultValue ? 1 : 0}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (meta.check) {
|
|
83
|
+
colDef += ` CHECK (${meta.check})`;
|
|
84
|
+
}
|
|
85
|
+
columns.push(colDef);
|
|
86
|
+
}
|
|
87
|
+
return `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
88
|
+
${columns.join(`,
|
|
89
|
+
`)}
|
|
90
|
+
)`;
|
|
91
|
+
}
|
|
92
|
+
function generateIndexSql(schema) {
|
|
93
|
+
const sqls = [];
|
|
94
|
+
const tableName = schema._name;
|
|
95
|
+
for (const index of schema._indexes) {
|
|
96
|
+
const indexName = index.name || `idx_${tableName}_${index.columns.join("_")}`;
|
|
97
|
+
const unique = index.unique ? "UNIQUE " : "";
|
|
98
|
+
sqls.push(`CREATE ${unique}INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${index.columns.join(", ")})`);
|
|
99
|
+
}
|
|
100
|
+
for (const [colName, colBuilder] of Object.entries(schema._columns)) {
|
|
101
|
+
const meta = colBuilder._meta;
|
|
102
|
+
if (meta.primary || meta.unique)
|
|
103
|
+
continue;
|
|
104
|
+
if (meta.sqlType === "boolean") {
|
|
105
|
+
sqls.push(`CREATE INDEX IF NOT EXISTS idx_${tableName}_${colName} ON ${tableName}(${colName})`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return sqls;
|
|
109
|
+
}
|
|
110
|
+
function convertValueForSql(value, sqlType) {
|
|
111
|
+
if (sqlType === "boolean") {
|
|
112
|
+
return value ? 1 : 0;
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
function buildWhereClause(where, columns) {
|
|
117
|
+
const clauses = [];
|
|
118
|
+
const params = [];
|
|
119
|
+
for (const [key, value] of Object.entries(where)) {
|
|
120
|
+
clauses.push(`${key} = ?`);
|
|
121
|
+
const colMeta = columns[key]?._meta;
|
|
122
|
+
const convertedValue = convertValueForSql(value, colMeta?.sqlType);
|
|
123
|
+
params.push(convertedValue);
|
|
124
|
+
}
|
|
125
|
+
return { clauses, params };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
class BaseSqlAdapter {
|
|
129
|
+
driver;
|
|
130
|
+
schema;
|
|
131
|
+
tableName;
|
|
132
|
+
constructor(driver, schema) {
|
|
133
|
+
this.driver = driver;
|
|
134
|
+
this.schema = schema;
|
|
135
|
+
this.tableName = schema._name;
|
|
136
|
+
}
|
|
137
|
+
getAllowedWhereColumns() {
|
|
138
|
+
const columns = new Set;
|
|
139
|
+
for (const colName of Object.keys(this.schema._columns)) {
|
|
140
|
+
columns.add(colName);
|
|
141
|
+
}
|
|
142
|
+
return columns;
|
|
143
|
+
}
|
|
144
|
+
convertRow(row) {
|
|
145
|
+
const converted = { ...row };
|
|
146
|
+
for (const [colName, colBuilder] of Object.entries(this.schema._columns)) {
|
|
147
|
+
const meta = colBuilder._meta;
|
|
148
|
+
if (meta.sqlType === "boolean" && row[colName] !== undefined) {
|
|
149
|
+
converted[colName] = Boolean(row[colName]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return converted;
|
|
153
|
+
}
|
|
154
|
+
convertValueForColumn(value, colName) {
|
|
155
|
+
const colBuilder = this.schema._columns[colName];
|
|
156
|
+
if (!colBuilder)
|
|
157
|
+
return value;
|
|
158
|
+
const meta = colBuilder._meta;
|
|
159
|
+
return convertValueForSql(value, meta?.sqlType);
|
|
160
|
+
}
|
|
161
|
+
async get(id) {
|
|
162
|
+
try {
|
|
163
|
+
const rows = await this.driver.query(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
164
|
+
if (!rows[0])
|
|
165
|
+
return null;
|
|
166
|
+
return this.convertRow(rows[0]);
|
|
167
|
+
} catch {
|
|
168
|
+
throw new Error(`Failed to retrieve record: resource may be unavailable`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async list(options) {
|
|
172
|
+
const allowedColumns = this.getAllowedWhereColumns();
|
|
173
|
+
try {
|
|
174
|
+
if (options?.where) {
|
|
175
|
+
for (const key of Object.keys(options.where)) {
|
|
176
|
+
if (!allowedColumns.has(key)) {
|
|
177
|
+
throw new Error(`Invalid filter column: ${key}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
let countSql = `SELECT COUNT(*) as count FROM ${this.tableName}`;
|
|
182
|
+
const countParams = [];
|
|
183
|
+
if (options?.where && Object.keys(options.where).length > 0) {
|
|
184
|
+
const { clauses, params: params2 } = buildWhereClause(options.where, this.schema._columns);
|
|
185
|
+
countSql += ` WHERE ${clauses.join(" AND ")}`;
|
|
186
|
+
countParams.push(...params2);
|
|
187
|
+
}
|
|
188
|
+
const countResult = await this.driver.query(countSql, countParams);
|
|
189
|
+
const total = Number(countResult[0]?.count ?? 0);
|
|
190
|
+
let sql = `SELECT * FROM ${this.tableName}`;
|
|
191
|
+
const params = [];
|
|
192
|
+
if (options?.where && Object.keys(options.where).length > 0) {
|
|
193
|
+
const { clauses, params: whereParams } = buildWhereClause(options.where, this.schema._columns);
|
|
194
|
+
sql += ` WHERE ${clauses.join(" AND ")}`;
|
|
195
|
+
params.push(...whereParams);
|
|
196
|
+
}
|
|
197
|
+
if (options?.after) {
|
|
198
|
+
sql += params.length > 0 ? " AND" : " WHERE";
|
|
199
|
+
sql += " id > ?";
|
|
200
|
+
params.push(options.after);
|
|
201
|
+
}
|
|
202
|
+
sql += " ORDER BY id ASC";
|
|
203
|
+
const limit = options?.limit ?? 20;
|
|
204
|
+
sql += " LIMIT ?";
|
|
205
|
+
params.push(limit);
|
|
206
|
+
const data = await this.driver.query(sql, params);
|
|
207
|
+
const convertedData = data.map((row) => this.convertRow(row));
|
|
208
|
+
return { data: convertedData, total };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof Error && error.message.startsWith("Invalid filter column:")) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`Failed to list records: please try again later`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async create(data) {
|
|
217
|
+
try {
|
|
218
|
+
const columns = [];
|
|
219
|
+
const placeholders = [];
|
|
220
|
+
const params = [];
|
|
221
|
+
for (const [colName, colBuilder] of Object.entries(this.schema._columns)) {
|
|
222
|
+
const meta = colBuilder._meta;
|
|
223
|
+
if (meta.isReadOnly && !meta.isAutoUpdate)
|
|
224
|
+
continue;
|
|
225
|
+
if (meta.primary && !data[colName] && meta.generate) {
|
|
226
|
+
data[colName] = generateId(meta.generate);
|
|
227
|
+
}
|
|
228
|
+
if (meta.isAutoUpdate) {
|
|
229
|
+
data[colName] = new Date().toISOString();
|
|
230
|
+
}
|
|
231
|
+
if (data[colName] !== undefined || meta.hasDefault) {
|
|
232
|
+
columns.push(colName);
|
|
233
|
+
placeholders.push("?");
|
|
234
|
+
let value = data[colName];
|
|
235
|
+
if (value === undefined && meta.hasDefault) {
|
|
236
|
+
if (meta.defaultValue === "now") {
|
|
237
|
+
value = new Date().toISOString();
|
|
238
|
+
} else {
|
|
239
|
+
value = meta.defaultValue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
value = this.convertValueForColumn(value, colName);
|
|
243
|
+
params.push(value);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await this.driver.execute(`INSERT INTO ${this.tableName} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`, params);
|
|
247
|
+
const id = data.id;
|
|
248
|
+
return this.get(id);
|
|
249
|
+
} catch {
|
|
250
|
+
throw new Error(`Failed to create record: please check your input`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async update(id, data) {
|
|
254
|
+
try {
|
|
255
|
+
const updates = [];
|
|
256
|
+
const params = [];
|
|
257
|
+
for (const [colName, colBuilder] of Object.entries(this.schema._columns)) {
|
|
258
|
+
const meta = colBuilder._meta;
|
|
259
|
+
if (meta.isReadOnly && !meta.isAutoUpdate || meta.primary)
|
|
260
|
+
continue;
|
|
261
|
+
if (meta.isAutoUpdate) {
|
|
262
|
+
updates.push(`${colName} = ?`);
|
|
263
|
+
params.push(new Date().toISOString());
|
|
264
|
+
}
|
|
265
|
+
if (data[colName] !== undefined) {
|
|
266
|
+
updates.push(`${colName} = ?`);
|
|
267
|
+
const value = this.convertValueForColumn(data[colName], colName);
|
|
268
|
+
params.push(value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (updates.length === 0) {
|
|
272
|
+
return this.get(id);
|
|
273
|
+
}
|
|
274
|
+
params.push(id);
|
|
275
|
+
await this.driver.execute(`UPDATE ${this.tableName} SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
276
|
+
const result = await this.get(id);
|
|
277
|
+
if (!result) {
|
|
278
|
+
throw new Error("Record not found");
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error instanceof Error && error.message === "Record not found") {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
throw new Error(`Failed to update record: please try again later`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async delete(id) {
|
|
289
|
+
try {
|
|
290
|
+
const existing = await this.get(id);
|
|
291
|
+
if (!existing) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
await this.driver.execute(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
295
|
+
return existing;
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error(`Failed to delete record: please try again later`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { generateId, generateCreateTableSql, generateIndexSql, BaseSqlAdapter };
|
|
@@ -44,7 +44,7 @@ var ERROR_PATTERNS = [
|
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
pattern: /Record not found in table ([^\s]+)/,
|
|
47
|
-
code: "
|
|
47
|
+
code: "NotFound",
|
|
48
48
|
explanation: "A getOrThrow, update, or delete query did not match any rows.",
|
|
49
49
|
suggestion: "Verify the where clause matches existing rows. Use get() if the record may not exist."
|
|
50
50
|
},
|
|
@@ -52,7 +52,7 @@ var ERROR_PATTERNS = [
|
|
|
52
52
|
pattern: /Table "([^"]+)" is not registered in the database/,
|
|
53
53
|
code: "UNREGISTERED_TABLE",
|
|
54
54
|
explanation: "The table name passed to a query method is not in the database registry.",
|
|
55
|
-
suggestion: "Register the table in the `
|
|
55
|
+
suggestion: "Register the table in the `models` record passed to `createDb()`."
|
|
56
56
|
}
|
|
57
57
|
];
|
|
58
58
|
function diagnoseError(message) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/migration/snapshot-storage.ts
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
|
|
5
|
+
class NodeSnapshotStorage {
|
|
6
|
+
async load(path) {
|
|
7
|
+
try {
|
|
8
|
+
const content = await readFile(path, "utf-8");
|
|
9
|
+
return JSON.parse(content);
|
|
10
|
+
} catch (err) {
|
|
11
|
+
if (err.code === "ENOENT") {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
throw err;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async save(path, snapshot) {
|
|
18
|
+
const dir = dirname(path);
|
|
19
|
+
await mkdir(dir, { recursive: true });
|
|
20
|
+
const content = JSON.stringify(snapshot, null, 2);
|
|
21
|
+
await writeFile(path, `${content}
|
|
22
|
+
`, "utf-8");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { NodeSnapshotStorage };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseSqlAdapter,
|
|
3
|
+
generateCreateTableSql,
|
|
4
|
+
generateIndexSql
|
|
5
|
+
} from "./chunk-fwk49jvg.js";
|
|
6
|
+
|
|
7
|
+
// src/adapters/d1-adapter.ts
|
|
8
|
+
function createD1Driver(d1) {
|
|
9
|
+
const query = async (sql, params) => {
|
|
10
|
+
const prepared = d1.prepare(sql);
|
|
11
|
+
const bound = params ? prepared.bind(...params) : prepared;
|
|
12
|
+
const result = await bound.all();
|
|
13
|
+
return result.results;
|
|
14
|
+
};
|
|
15
|
+
const execute = async (sql, params) => {
|
|
16
|
+
const prepared = d1.prepare(sql);
|
|
17
|
+
const bound = params ? prepared.bind(...params) : prepared;
|
|
18
|
+
const result = await bound.run();
|
|
19
|
+
return { rowsAffected: result.meta.changes };
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
query,
|
|
23
|
+
execute,
|
|
24
|
+
close: async () => {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class D1Adapter extends BaseSqlAdapter {
|
|
29
|
+
constructor(driver, schema) {
|
|
30
|
+
super(driver, schema);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function createD1Adapter(options) {
|
|
34
|
+
const { schema, d1, migrations } = options;
|
|
35
|
+
const driver = createD1Driver(d1);
|
|
36
|
+
if (migrations?.autoApply) {
|
|
37
|
+
const createTableSql = generateCreateTableSql(schema);
|
|
38
|
+
driver.execute(createTableSql);
|
|
39
|
+
const indexSqls = generateIndexSql(schema);
|
|
40
|
+
for (const sql of indexSqls) {
|
|
41
|
+
driver.execute(sql);
|
|
42
|
+
}
|
|
43
|
+
console.log(`\uD83D\uDCE6 D1 database adapter initialized for table: ${schema._name}`);
|
|
44
|
+
}
|
|
45
|
+
return new D1Adapter(driver, schema);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { createD1Driver, createD1Adapter };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-j4kwq1gh.js";
|
|
4
|
+
|
|
5
|
+
// src/client/postgres-driver.ts
|
|
6
|
+
function loadPostgres() {
|
|
7
|
+
try {
|
|
8
|
+
const mod = __require("postgres");
|
|
9
|
+
return typeof mod === "function" ? mod : mod.default;
|
|
10
|
+
} catch {
|
|
11
|
+
throw new Error('The "postgres" package is required for PostgreSQL. Install: bun add postgres');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isPostgresError(error) {
|
|
15
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string";
|
|
16
|
+
}
|
|
17
|
+
function adaptPostgresError(error) {
|
|
18
|
+
if (isPostgresError(error)) {
|
|
19
|
+
const adapted = Object.assign(new Error(error.message), {
|
|
20
|
+
code: error.code,
|
|
21
|
+
message: error.message,
|
|
22
|
+
table: error.table_name,
|
|
23
|
+
column: error.column_name,
|
|
24
|
+
constraint: error.constraint_name,
|
|
25
|
+
detail: error.detail
|
|
26
|
+
});
|
|
27
|
+
throw adapted;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
function createPostgresDriver(url, pool) {
|
|
32
|
+
const sql = loadPostgres()(url, {
|
|
33
|
+
max: pool?.max ?? 10,
|
|
34
|
+
idle_timeout: pool?.idleTimeout !== undefined ? pool.idleTimeout / 1000 : 30,
|
|
35
|
+
connect_timeout: pool?.connectionTimeout !== undefined ? pool.connectionTimeout / 1000 : 10,
|
|
36
|
+
fetch_types: false
|
|
37
|
+
});
|
|
38
|
+
const queryFn = async (sqlStr, params) => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await sql.unsafe(sqlStr, params);
|
|
41
|
+
const rows = result.map((row) => {
|
|
42
|
+
const mapped = {};
|
|
43
|
+
for (const [key, value] of Object.entries(row)) {
|
|
44
|
+
mapped[key] = coerceValue(value);
|
|
45
|
+
}
|
|
46
|
+
return mapped;
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
rows,
|
|
50
|
+
rowCount: result.count ?? rows.length
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
adaptPostgresError(error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
queryFn,
|
|
58
|
+
query: async (sql2, params) => {
|
|
59
|
+
const result = await queryFn(sql2, params ?? []);
|
|
60
|
+
return result.rows;
|
|
61
|
+
},
|
|
62
|
+
execute: async (sql2, params) => {
|
|
63
|
+
const result = await queryFn(sql2, params ?? []);
|
|
64
|
+
return { rowsAffected: result.rowCount };
|
|
65
|
+
},
|
|
66
|
+
async close() {
|
|
67
|
+
await sql.end();
|
|
68
|
+
},
|
|
69
|
+
async isHealthy() {
|
|
70
|
+
let timer;
|
|
71
|
+
try {
|
|
72
|
+
const healthCheckTimeout = pool?.healthCheckTimeout ?? 5000;
|
|
73
|
+
const timeout = new Promise((_, reject) => {
|
|
74
|
+
timer = setTimeout(() => reject(new Error("Health check timed out")), healthCheckTimeout);
|
|
75
|
+
});
|
|
76
|
+
await Promise.race([sql`SELECT 1`, timeout]);
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
} finally {
|
|
81
|
+
if (timer !== undefined)
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function coerceValue(value) {
|
|
88
|
+
if (typeof value === "string" && isTimestampString(value)) {
|
|
89
|
+
const date = new Date(value);
|
|
90
|
+
if (!Number.isNaN(date.getTime())) {
|
|
91
|
+
return date;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function isTimestampString(value) {
|
|
97
|
+
return /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { createPostgresDriver };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/util/hash.ts
|
|
2
|
+
async function sha256Hex(input) {
|
|
3
|
+
const data = new TextEncoder().encode(input);
|
|
4
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
5
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
6
|
+
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { sha256Hex };
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
// src/sql/casing.ts
|
|
2
|
-
function camelToSnake(str) {
|
|
2
|
+
function camelToSnake(str, overrides) {
|
|
3
3
|
if (str.length === 0)
|
|
4
4
|
return str;
|
|
5
|
+
if (overrides && str in overrides) {
|
|
6
|
+
return overrides[str];
|
|
7
|
+
}
|
|
5
8
|
return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase();
|
|
6
9
|
}
|
|
7
|
-
function snakeToCamel(str) {
|
|
10
|
+
function snakeToCamel(str, overrides) {
|
|
8
11
|
if (str.length === 0)
|
|
9
12
|
return str;
|
|
13
|
+
if (overrides) {
|
|
14
|
+
for (const [camelKey, snakeVal] of Object.entries(overrides)) {
|
|
15
|
+
if (snakeVal === str) {
|
|
16
|
+
return camelKey;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
return str.replace(/([a-zA-Z\d])_([a-zA-Z])/g, (_, prev, char) => prev + char.toUpperCase());
|
|
11
21
|
}
|
|
12
22
|
|