@vertz/db 0.2.0 → 0.2.3
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 +1759 -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-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-pnk6yzjv.js +48 -0
- package/dist/shared/chunk-rqe0prft.js +100 -0
- package/dist/shared/chunk-sfmyxz6r.js +306 -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 +32 -5
|
@@ -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,306 @@
|
|
|
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.replace(/'/g, "''")}'`;
|
|
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
|
+
const DANGEROUS_PATTERN = /;|--|\b(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE|EXEC)\b/i;
|
|
84
|
+
if (DANGEROUS_PATTERN.test(meta.check)) {
|
|
85
|
+
throw new Error(`Unsafe CHECK constraint expression: "${meta.check}"`);
|
|
86
|
+
}
|
|
87
|
+
colDef += ` CHECK (${meta.check})`;
|
|
88
|
+
}
|
|
89
|
+
columns.push(colDef);
|
|
90
|
+
}
|
|
91
|
+
return `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
92
|
+
${columns.join(`,
|
|
93
|
+
`)}
|
|
94
|
+
)`;
|
|
95
|
+
}
|
|
96
|
+
function generateIndexSql(schema) {
|
|
97
|
+
const sqls = [];
|
|
98
|
+
const tableName = schema._name;
|
|
99
|
+
for (const index of schema._indexes) {
|
|
100
|
+
const indexName = index.name || `idx_${tableName}_${index.columns.join("_")}`;
|
|
101
|
+
const unique = index.unique ? "UNIQUE " : "";
|
|
102
|
+
sqls.push(`CREATE ${unique}INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${index.columns.join(", ")})`);
|
|
103
|
+
}
|
|
104
|
+
for (const [colName, colBuilder] of Object.entries(schema._columns)) {
|
|
105
|
+
const meta = colBuilder._meta;
|
|
106
|
+
if (meta.primary || meta.unique)
|
|
107
|
+
continue;
|
|
108
|
+
if (meta.sqlType === "boolean") {
|
|
109
|
+
sqls.push(`CREATE INDEX IF NOT EXISTS idx_${tableName}_${colName} ON ${tableName}(${colName})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return sqls;
|
|
113
|
+
}
|
|
114
|
+
function convertValueForSql(value, sqlType) {
|
|
115
|
+
if (sqlType === "boolean") {
|
|
116
|
+
return value ? 1 : 0;
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function buildWhereClause(where, columns) {
|
|
121
|
+
const clauses = [];
|
|
122
|
+
const params = [];
|
|
123
|
+
for (const [key, value] of Object.entries(where)) {
|
|
124
|
+
clauses.push(`${key} = ?`);
|
|
125
|
+
const colMeta = columns[key]?._meta;
|
|
126
|
+
const convertedValue = convertValueForSql(value, colMeta?.sqlType);
|
|
127
|
+
params.push(convertedValue);
|
|
128
|
+
}
|
|
129
|
+
return { clauses, params };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class BaseSqlAdapter {
|
|
133
|
+
driver;
|
|
134
|
+
schema;
|
|
135
|
+
tableName;
|
|
136
|
+
constructor(driver, schema) {
|
|
137
|
+
this.driver = driver;
|
|
138
|
+
this.schema = schema;
|
|
139
|
+
this.tableName = schema._name;
|
|
140
|
+
}
|
|
141
|
+
getAllowedWhereColumns() {
|
|
142
|
+
const columns = new Set;
|
|
143
|
+
for (const colName of Object.keys(this.schema._columns)) {
|
|
144
|
+
columns.add(colName);
|
|
145
|
+
}
|
|
146
|
+
return columns;
|
|
147
|
+
}
|
|
148
|
+
convertRow(row) {
|
|
149
|
+
const converted = { ...row };
|
|
150
|
+
for (const [colName, colBuilder] of Object.entries(this.schema._columns)) {
|
|
151
|
+
const meta = colBuilder._meta;
|
|
152
|
+
if (meta.sqlType === "boolean" && row[colName] !== undefined) {
|
|
153
|
+
converted[colName] = Boolean(row[colName]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return converted;
|
|
157
|
+
}
|
|
158
|
+
convertValueForColumn(value, colName) {
|
|
159
|
+
const colBuilder = this.schema._columns[colName];
|
|
160
|
+
if (!colBuilder)
|
|
161
|
+
return value;
|
|
162
|
+
const meta = colBuilder._meta;
|
|
163
|
+
return convertValueForSql(value, meta?.sqlType);
|
|
164
|
+
}
|
|
165
|
+
async get(id) {
|
|
166
|
+
try {
|
|
167
|
+
const rows = await this.driver.query(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
168
|
+
if (!rows[0])
|
|
169
|
+
return null;
|
|
170
|
+
return this.convertRow(rows[0]);
|
|
171
|
+
} catch {
|
|
172
|
+
throw new Error(`Failed to retrieve record: resource may be unavailable`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async list(options) {
|
|
176
|
+
const allowedColumns = this.getAllowedWhereColumns();
|
|
177
|
+
try {
|
|
178
|
+
if (options?.where) {
|
|
179
|
+
for (const key of Object.keys(options.where)) {
|
|
180
|
+
if (!allowedColumns.has(key)) {
|
|
181
|
+
throw new Error(`Invalid filter column: ${key}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let countSql = `SELECT COUNT(*) as count FROM ${this.tableName}`;
|
|
186
|
+
const countParams = [];
|
|
187
|
+
if (options?.where && Object.keys(options.where).length > 0) {
|
|
188
|
+
const { clauses, params: params2 } = buildWhereClause(options.where, this.schema._columns);
|
|
189
|
+
countSql += ` WHERE ${clauses.join(" AND ")}`;
|
|
190
|
+
countParams.push(...params2);
|
|
191
|
+
}
|
|
192
|
+
const countResult = await this.driver.query(countSql, countParams);
|
|
193
|
+
const total = Number(countResult[0]?.count ?? 0);
|
|
194
|
+
let sql = `SELECT * FROM ${this.tableName}`;
|
|
195
|
+
const params = [];
|
|
196
|
+
if (options?.where && Object.keys(options.where).length > 0) {
|
|
197
|
+
const { clauses, params: whereParams } = buildWhereClause(options.where, this.schema._columns);
|
|
198
|
+
sql += ` WHERE ${clauses.join(" AND ")}`;
|
|
199
|
+
params.push(...whereParams);
|
|
200
|
+
}
|
|
201
|
+
if (options?.after) {
|
|
202
|
+
sql += params.length > 0 ? " AND" : " WHERE";
|
|
203
|
+
sql += " id > ?";
|
|
204
|
+
params.push(options.after);
|
|
205
|
+
}
|
|
206
|
+
sql += " ORDER BY id ASC";
|
|
207
|
+
const limit = options?.limit ?? 20;
|
|
208
|
+
sql += " LIMIT ?";
|
|
209
|
+
params.push(limit);
|
|
210
|
+
const data = await this.driver.query(sql, params);
|
|
211
|
+
const convertedData = data.map((row) => this.convertRow(row));
|
|
212
|
+
return { data: convertedData, total };
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof Error && error.message.startsWith("Invalid filter column:")) {
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Failed to list records: please try again later`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async create(data) {
|
|
221
|
+
try {
|
|
222
|
+
const columns = [];
|
|
223
|
+
const placeholders = [];
|
|
224
|
+
const params = [];
|
|
225
|
+
for (const [colName, colBuilder] of Object.entries(this.schema._columns)) {
|
|
226
|
+
const meta = colBuilder._meta;
|
|
227
|
+
if (meta.isReadOnly && !meta.isAutoUpdate)
|
|
228
|
+
continue;
|
|
229
|
+
if (meta.primary && !data[colName] && meta.generate) {
|
|
230
|
+
data[colName] = generateId(meta.generate);
|
|
231
|
+
}
|
|
232
|
+
if (meta.isAutoUpdate) {
|
|
233
|
+
data[colName] = new Date().toISOString();
|
|
234
|
+
}
|
|
235
|
+
if (data[colName] !== undefined || meta.hasDefault) {
|
|
236
|
+
columns.push(colName);
|
|
237
|
+
placeholders.push("?");
|
|
238
|
+
let value = data[colName];
|
|
239
|
+
if (value === undefined && meta.hasDefault) {
|
|
240
|
+
if (meta.defaultValue === "now") {
|
|
241
|
+
value = new Date().toISOString();
|
|
242
|
+
} else {
|
|
243
|
+
value = meta.defaultValue;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
value = this.convertValueForColumn(value, colName);
|
|
247
|
+
params.push(value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
await this.driver.execute(`INSERT INTO ${this.tableName} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`, params);
|
|
251
|
+
const id = data.id;
|
|
252
|
+
return this.get(id);
|
|
253
|
+
} catch {
|
|
254
|
+
throw new Error(`Failed to create record: please check your input`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async update(id, data) {
|
|
258
|
+
try {
|
|
259
|
+
const updates = [];
|
|
260
|
+
const params = [];
|
|
261
|
+
for (const [colName, colBuilder] of Object.entries(this.schema._columns)) {
|
|
262
|
+
const meta = colBuilder._meta;
|
|
263
|
+
if (meta.isReadOnly && !meta.isAutoUpdate || meta.primary)
|
|
264
|
+
continue;
|
|
265
|
+
if (meta.isAutoUpdate) {
|
|
266
|
+
updates.push(`${colName} = ?`);
|
|
267
|
+
params.push(new Date().toISOString());
|
|
268
|
+
}
|
|
269
|
+
if (data[colName] !== undefined) {
|
|
270
|
+
updates.push(`${colName} = ?`);
|
|
271
|
+
const value = this.convertValueForColumn(data[colName], colName);
|
|
272
|
+
params.push(value);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (updates.length === 0) {
|
|
276
|
+
return this.get(id);
|
|
277
|
+
}
|
|
278
|
+
params.push(id);
|
|
279
|
+
await this.driver.execute(`UPDATE ${this.tableName} SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
280
|
+
const result = await this.get(id);
|
|
281
|
+
if (!result) {
|
|
282
|
+
throw new Error("Record not found");
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error instanceof Error && error.message === "Record not found") {
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
throw new Error(`Failed to update record: please try again later`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async delete(id) {
|
|
293
|
+
try {
|
|
294
|
+
const existing = await this.get(id);
|
|
295
|
+
if (!existing) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
await this.driver.execute(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);
|
|
299
|
+
return existing;
|
|
300
|
+
} catch {
|
|
301
|
+
throw new Error(`Failed to delete record: please try again later`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export { generateId, generateCreateTableSql, generateIndexSql, BaseSqlAdapter };
|
|
@@ -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
|
|
package/dist/sql/index.d.ts
CHANGED
|
@@ -5,33 +5,69 @@
|
|
|
5
5
|
* to PostgreSQL column names (snake_case) and vice versa.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
+
* Override map for custom casing conversions.
|
|
9
|
+
* Keys are camelCase, values are snake_case.
|
|
10
|
+
* Example: { 'oAuth': 'oauth', 'userID': 'user_id' }
|
|
11
|
+
*/
|
|
12
|
+
type CasingOverrides = Record<string, string>;
|
|
13
|
+
/**
|
|
8
14
|
* Convert a camelCase string to snake_case.
|
|
9
15
|
*
|
|
10
16
|
* Handles acronyms correctly:
|
|
11
17
|
* - "parseJSON" -> "parse_json"
|
|
12
18
|
* - "getHTTPSUrl" -> "get_https_url"
|
|
13
19
|
* - "htmlParser" -> "html_parser"
|
|
20
|
+
*
|
|
21
|
+
* @param str - The camelCase string to convert
|
|
22
|
+
* @param overrides - Optional map of camelCase -> snake_case overrides that take precedence
|
|
14
23
|
*/
|
|
15
|
-
declare function camelToSnake(str: string): string;
|
|
24
|
+
declare function camelToSnake(str: string, overrides?: CasingOverrides): string;
|
|
16
25
|
/**
|
|
17
26
|
* Convert a snake_case string to camelCase.
|
|
18
27
|
*
|
|
19
28
|
* - "first_name" -> "firstName"
|
|
20
29
|
* - "created_at_timestamp" -> "createdAtTimestamp"
|
|
21
|
-
*/
|
|
22
|
-
declare function snakeToCamel(str: string): string;
|
|
23
|
-
/**
|
|
24
|
-
* DELETE statement builder.
|
|
25
30
|
*
|
|
26
|
-
*
|
|
27
|
-
* -
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
* @param str - The snake_case string to convert
|
|
32
|
+
* @param overrides - Optional map of camelCase -> snake_case overrides (reverse lookup)
|
|
33
|
+
*/
|
|
34
|
+
declare function snakeToCamel(str: string, overrides?: CasingOverrides): string;
|
|
35
|
+
interface Dialect {
|
|
36
|
+
/** Dialect name. */
|
|
37
|
+
readonly name: "postgres" | "sqlite";
|
|
38
|
+
/**
|
|
39
|
+
* Parameter placeholder: $1, $2 (postgres) or ? (sqlite).
|
|
40
|
+
* @param index - 1-based parameter index
|
|
41
|
+
*/
|
|
42
|
+
param(index: number): string;
|
|
43
|
+
/** SQL function for current timestamp. */
|
|
44
|
+
now(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Map a vertz column sqlType to the dialect's SQL type.
|
|
47
|
+
* @param sqlType - The generic sqlType from column metadata
|
|
48
|
+
* @param meta - Additional metadata (enum values, length, precision)
|
|
49
|
+
*/
|
|
50
|
+
mapColumnType(sqlType: string, meta?: ColumnTypeMeta): string;
|
|
51
|
+
/** Whether the dialect supports RETURNING clause. */
|
|
52
|
+
readonly supportsReturning: boolean;
|
|
53
|
+
/** Whether the dialect supports array operators (@>, <@, &&). */
|
|
54
|
+
readonly supportsArrayOps: boolean;
|
|
55
|
+
/** Whether the dialect supports JSONB path operators (->>, ->). */
|
|
56
|
+
readonly supportsJsonbPath: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface ColumnTypeMeta {
|
|
59
|
+
readonly enumName?: string;
|
|
60
|
+
readonly enumValues?: readonly string[];
|
|
61
|
+
readonly length?: number;
|
|
62
|
+
readonly precision?: number;
|
|
63
|
+
readonly scale?: number;
|
|
64
|
+
}
|
|
31
65
|
interface DeleteOptions {
|
|
32
66
|
readonly table: string;
|
|
33
67
|
readonly where?: Record<string, unknown>;
|
|
34
68
|
readonly returning?: "*" | readonly string[];
|
|
69
|
+
/** SQL dialect to use. Defaults to postgres. */
|
|
70
|
+
readonly dialect?: Dialect;
|
|
35
71
|
}
|
|
36
72
|
interface DeleteResult {
|
|
37
73
|
readonly sql: string;
|
|
@@ -40,17 +76,7 @@ interface DeleteResult {
|
|
|
40
76
|
/**
|
|
41
77
|
* Build a DELETE statement from the given options.
|
|
42
78
|
*/
|
|
43
|
-
declare function buildDelete(options: DeleteOptions): DeleteResult;
|
|
44
|
-
/**
|
|
45
|
-
* INSERT statement builder.
|
|
46
|
-
*
|
|
47
|
-
* Generates parameterized INSERT queries with support for:
|
|
48
|
-
* - Single row and batch (multi-row VALUES) inserts
|
|
49
|
-
* - RETURNING clause with column aliasing
|
|
50
|
-
* - ON CONFLICT (upsert) — DO NOTHING or DO UPDATE SET
|
|
51
|
-
* - camelCase -> snake_case column conversion
|
|
52
|
-
* - "now" sentinel handling for timestamp defaults
|
|
53
|
-
*/
|
|
79
|
+
declare function buildDelete(options: DeleteOptions, dialect?: Dialect): DeleteResult;
|
|
54
80
|
interface OnConflictOptions {
|
|
55
81
|
readonly columns: readonly string[];
|
|
56
82
|
readonly action: "nothing" | "update";
|
|
@@ -65,6 +91,8 @@ interface InsertOptions {
|
|
|
65
91
|
readonly onConflict?: OnConflictOptions;
|
|
66
92
|
/** Column names (camelCase) that should use NOW() instead of a parameterized value when the value is "now". */
|
|
67
93
|
readonly nowColumns?: readonly string[];
|
|
94
|
+
/** SQL dialect to use. Defaults to postgres. */
|
|
95
|
+
readonly dialect?: Dialect;
|
|
68
96
|
}
|
|
69
97
|
interface InsertResult {
|
|
70
98
|
readonly sql: string;
|
|
@@ -73,18 +101,7 @@ interface InsertResult {
|
|
|
73
101
|
/**
|
|
74
102
|
* Build an INSERT statement from the given options.
|
|
75
103
|
*/
|
|
76
|
-
declare function buildInsert(options: InsertOptions): InsertResult;
|
|
77
|
-
/**
|
|
78
|
-
* SELECT statement builder.
|
|
79
|
-
*
|
|
80
|
-
* Generates parameterized SELECT queries with support for:
|
|
81
|
-
* - Column selection with camelCase -> snake_case conversion and aliasing
|
|
82
|
-
* - WHERE clause via the where builder
|
|
83
|
-
* - ORDER BY with direction
|
|
84
|
-
* - LIMIT / OFFSET pagination (parameterized)
|
|
85
|
-
* - Cursor-based pagination (cursor + take)
|
|
86
|
-
* - COUNT(*) OVER() for listAndCount
|
|
87
|
-
*/
|
|
104
|
+
declare function buildInsert(options: InsertOptions, dialect?: Dialect): InsertResult;
|
|
88
105
|
interface SelectOptions {
|
|
89
106
|
readonly table: string;
|
|
90
107
|
readonly columns?: readonly string[];
|
|
@@ -97,6 +114,10 @@ interface SelectOptions {
|
|
|
97
114
|
readonly cursor?: Record<string, unknown>;
|
|
98
115
|
/** Number of rows to take (used with cursor). Aliases `limit` when cursor is present. */
|
|
99
116
|
readonly take?: number;
|
|
117
|
+
/** Custom casing overrides for camelCase -> snake_case conversion. */
|
|
118
|
+
readonly casingOverrides?: CasingOverrides;
|
|
119
|
+
/** SQL dialect to use. Defaults to postgres. */
|
|
120
|
+
readonly dialect?: Dialect;
|
|
100
121
|
}
|
|
101
122
|
interface SelectResult {
|
|
102
123
|
readonly sql: string;
|
|
@@ -105,7 +126,7 @@ interface SelectResult {
|
|
|
105
126
|
/**
|
|
106
127
|
* Build a SELECT statement from the given options.
|
|
107
128
|
*/
|
|
108
|
-
declare function buildSelect(options: SelectOptions): SelectResult;
|
|
129
|
+
declare function buildSelect(options: SelectOptions, dialect?: Dialect): SelectResult;
|
|
109
130
|
/**
|
|
110
131
|
* SQL tagged template literal and escape hatch.
|
|
111
132
|
*
|
|
@@ -151,16 +172,6 @@ declare const sql: {
|
|
|
151
172
|
(strings: TemplateStringsArray, ...values: unknown[]): SqlFragment;
|
|
152
173
|
raw(value: string): SqlFragment;
|
|
153
174
|
};
|
|
154
|
-
/**
|
|
155
|
-
* UPDATE statement builder.
|
|
156
|
-
*
|
|
157
|
-
* Generates parameterized UPDATE queries with support for:
|
|
158
|
-
* - SET clause from a data object
|
|
159
|
-
* - WHERE clause via the where builder
|
|
160
|
-
* - RETURNING clause with column aliasing
|
|
161
|
-
* - camelCase -> snake_case column conversion
|
|
162
|
-
* - "now" sentinel handling for timestamp defaults
|
|
163
|
-
*/
|
|
164
175
|
interface UpdateOptions {
|
|
165
176
|
readonly table: string;
|
|
166
177
|
readonly data: Record<string, unknown>;
|
|
@@ -168,6 +179,8 @@ interface UpdateOptions {
|
|
|
168
179
|
readonly returning?: "*" | readonly string[];
|
|
169
180
|
/** Column names (camelCase) that should use NOW() instead of a parameterized value when the value is "now". */
|
|
170
181
|
readonly nowColumns?: readonly string[];
|
|
182
|
+
/** SQL dialect to use. Defaults to postgres. */
|
|
183
|
+
readonly dialect?: Dialect;
|
|
171
184
|
}
|
|
172
185
|
interface UpdateResult {
|
|
173
186
|
readonly sql: string;
|
|
@@ -176,22 +189,7 @@ interface UpdateResult {
|
|
|
176
189
|
/**
|
|
177
190
|
* Build an UPDATE statement from the given options.
|
|
178
191
|
*/
|
|
179
|
-
declare function buildUpdate(options: UpdateOptions): UpdateResult;
|
|
180
|
-
/**
|
|
181
|
-
* WHERE clause builder with parameterized queries.
|
|
182
|
-
*
|
|
183
|
-
* Supports all filter operators from the schema layer:
|
|
184
|
-
* - Comparison: eq, ne, gt, gte, lt, lte
|
|
185
|
-
* - String: contains, startsWith, endsWith
|
|
186
|
-
* - Set: in, notIn
|
|
187
|
-
* - Null: isNull (true/false)
|
|
188
|
-
* - Logical: AND, OR, NOT
|
|
189
|
-
* - PostgreSQL array: arrayContains (@>), arrayContainedBy (<@), arrayOverlaps (&&)
|
|
190
|
-
* - JSONB path: metadata->key syntax
|
|
191
|
-
*
|
|
192
|
-
* All values are parameterized ($1, $2, ...) to prevent SQL injection.
|
|
193
|
-
* Column names are converted from camelCase to snake_case.
|
|
194
|
-
*/
|
|
192
|
+
declare function buildUpdate(options: UpdateOptions, dialect?: Dialect): UpdateResult;
|
|
195
193
|
interface WhereResult {
|
|
196
194
|
readonly sql: string;
|
|
197
195
|
readonly params: readonly unknown[];
|
|
@@ -207,7 +205,9 @@ interface WhereFilter {
|
|
|
207
205
|
*
|
|
208
206
|
* @param filter - The filter object with column conditions
|
|
209
207
|
* @param paramOffset - Starting parameter offset (0-based, params start at $offset+1)
|
|
208
|
+
* @param overrides - Optional casing overrides for camelCase -> snake_case conversion
|
|
209
|
+
* @param dialect - SQL dialect for parameter placeholders and feature checks
|
|
210
210
|
* @returns WhereResult with the SQL string (without WHERE keyword) and parameter values
|
|
211
211
|
*/
|
|
212
|
-
declare function buildWhere(filter: WhereFilter | undefined, paramOffset?: number): WhereResult;
|
|
212
|
+
declare function buildWhere(filter: WhereFilter | undefined, paramOffset?: number, overrides?: CasingOverrides, dialect?: Dialect): WhereResult;
|
|
213
213
|
export { sql, snakeToCamel, camelToSnake, buildWhere, buildUpdate, buildSelect, buildInsert, buildDelete, WhereResult, UpdateResult, UpdateOptions, SqlFragment, SelectResult, SelectOptions, OnConflictOptions, InsertResult, InsertOptions, DeleteResult, DeleteOptions };
|
package/dist/sql/index.js
CHANGED
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
buildSelect,
|
|
5
5
|
buildUpdate,
|
|
6
6
|
buildWhere
|
|
7
|
-
} from "../shared/chunk-
|
|
7
|
+
} from "../shared/chunk-0e1vy9qd.js";
|
|
8
8
|
import {
|
|
9
9
|
camelToSnake,
|
|
10
10
|
snakeToCamel
|
|
11
|
-
} from "../shared/chunk-
|
|
11
|
+
} from "../shared/chunk-v2qm94qp.js";
|
|
12
12
|
// src/sql/tagged.ts
|
|
13
13
|
function isSqlFragment(value) {
|
|
14
14
|
return typeof value === "object" && value !== null && "_tag" in value && value._tag === "SqlFragment";
|