@vertz/db 0.2.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 +923 -0
- package/dist/diagnostic/index.d.ts +41 -0
- package/dist/diagnostic/index.js +10 -0
- package/dist/index.d.ts +1346 -0
- package/dist/index.js +2010 -0
- package/dist/internals.d.ts +223 -0
- package/dist/internals.js +25 -0
- package/dist/plugin/index.d.ts +66 -0
- package/dist/plugin/index.js +66 -0
- package/dist/shared/chunk-3f2grpak.js +428 -0
- package/dist/shared/chunk-hrfdj0rr.js +13 -0
- package/dist/shared/chunk-wj026daz.js +86 -0
- package/dist/shared/chunk-xp022dyp.js +296 -0
- package/dist/sql/index.d.ts +213 -0
- package/dist/sql/index.js +64 -0
- package/package.json +72 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
snakeToCamel
|
|
3
|
+
} from "./chunk-hrfdj0rr.js";
|
|
4
|
+
|
|
5
|
+
// src/errors/db-error.ts
|
|
6
|
+
class DbError extends Error {
|
|
7
|
+
pgCode;
|
|
8
|
+
table;
|
|
9
|
+
query;
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
}
|
|
14
|
+
toJSON() {
|
|
15
|
+
const json = {
|
|
16
|
+
error: this.name,
|
|
17
|
+
code: this.code,
|
|
18
|
+
message: this.message
|
|
19
|
+
};
|
|
20
|
+
if (this.table !== undefined) {
|
|
21
|
+
json.table = this.table;
|
|
22
|
+
}
|
|
23
|
+
return json;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class UniqueConstraintError extends DbError {
|
|
28
|
+
code = "UNIQUE_VIOLATION";
|
|
29
|
+
pgCode = "23505";
|
|
30
|
+
table;
|
|
31
|
+
query;
|
|
32
|
+
column;
|
|
33
|
+
value;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
super(`Unique constraint violated on ${options.table}.${options.column}${options.value !== undefined ? ` (value: ${options.value})` : ""}`);
|
|
36
|
+
this.table = options.table;
|
|
37
|
+
this.column = options.column;
|
|
38
|
+
this.value = options.value;
|
|
39
|
+
this.query = options.query;
|
|
40
|
+
}
|
|
41
|
+
toJSON() {
|
|
42
|
+
return {
|
|
43
|
+
...super.toJSON(),
|
|
44
|
+
table: this.table,
|
|
45
|
+
column: this.column
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class ForeignKeyError extends DbError {
|
|
51
|
+
code = "FOREIGN_KEY_VIOLATION";
|
|
52
|
+
pgCode = "23503";
|
|
53
|
+
table;
|
|
54
|
+
query;
|
|
55
|
+
constraint;
|
|
56
|
+
detail;
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super(`Foreign key constraint "${options.constraint}" violated on table ${options.table}`);
|
|
59
|
+
this.table = options.table;
|
|
60
|
+
this.constraint = options.constraint;
|
|
61
|
+
this.detail = options.detail;
|
|
62
|
+
this.query = options.query;
|
|
63
|
+
}
|
|
64
|
+
toJSON() {
|
|
65
|
+
return {
|
|
66
|
+
...super.toJSON(),
|
|
67
|
+
table: this.table
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class NotNullError extends DbError {
|
|
73
|
+
code = "NOT_NULL_VIOLATION";
|
|
74
|
+
pgCode = "23502";
|
|
75
|
+
table;
|
|
76
|
+
query;
|
|
77
|
+
column;
|
|
78
|
+
constructor(options) {
|
|
79
|
+
super(`Not-null constraint violated on ${options.table}.${options.column}`);
|
|
80
|
+
this.table = options.table;
|
|
81
|
+
this.column = options.column;
|
|
82
|
+
this.query = options.query;
|
|
83
|
+
}
|
|
84
|
+
toJSON() {
|
|
85
|
+
return {
|
|
86
|
+
...super.toJSON(),
|
|
87
|
+
table: this.table,
|
|
88
|
+
column: this.column
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class CheckConstraintError extends DbError {
|
|
94
|
+
code = "CHECK_VIOLATION";
|
|
95
|
+
pgCode = "23514";
|
|
96
|
+
table;
|
|
97
|
+
query;
|
|
98
|
+
constraint;
|
|
99
|
+
constructor(options) {
|
|
100
|
+
super(`Check constraint "${options.constraint}" violated on table ${options.table}`);
|
|
101
|
+
this.table = options.table;
|
|
102
|
+
this.constraint = options.constraint;
|
|
103
|
+
this.query = options.query;
|
|
104
|
+
}
|
|
105
|
+
toJSON() {
|
|
106
|
+
return {
|
|
107
|
+
...super.toJSON(),
|
|
108
|
+
table: this.table
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class NotFoundError extends DbError {
|
|
114
|
+
code = "NOT_FOUND";
|
|
115
|
+
table;
|
|
116
|
+
query;
|
|
117
|
+
constructor(table, query) {
|
|
118
|
+
super(`Record not found in table ${table}`);
|
|
119
|
+
this.table = table;
|
|
120
|
+
this.query = query;
|
|
121
|
+
}
|
|
122
|
+
toJSON() {
|
|
123
|
+
return {
|
|
124
|
+
...super.toJSON(),
|
|
125
|
+
table: this.table
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class ConnectionError extends DbError {
|
|
131
|
+
code = "CONNECTION_ERROR";
|
|
132
|
+
constructor(message) {
|
|
133
|
+
super(`Database connection error: ${message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class ConnectionPoolExhaustedError extends ConnectionError {
|
|
138
|
+
code = "POOL_EXHAUSTED";
|
|
139
|
+
constructor(poolSize) {
|
|
140
|
+
super(`Connection pool exhausted (max: ${poolSize})`);
|
|
141
|
+
this.name = "ConnectionPoolExhaustedError";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/errors/pg-parser.ts
|
|
146
|
+
function extractKeyDetail(detail) {
|
|
147
|
+
if (!detail)
|
|
148
|
+
return null;
|
|
149
|
+
const match = detail.match(/^Key \(([^)]+)\)=\(([^)]*)\)/);
|
|
150
|
+
if (!match)
|
|
151
|
+
return null;
|
|
152
|
+
return { column: match[1], value: match[2] };
|
|
153
|
+
}
|
|
154
|
+
function extractNotNullColumn(message) {
|
|
155
|
+
const match = message.match(/null value in column "([^"]+)"/);
|
|
156
|
+
return match ? match[1] : null;
|
|
157
|
+
}
|
|
158
|
+
function extractCheckConstraint(message) {
|
|
159
|
+
const match = message.match(/violates check constraint "([^"]+)"/);
|
|
160
|
+
return match ? match[1] : null;
|
|
161
|
+
}
|
|
162
|
+
function isConnectionErrorCode(code) {
|
|
163
|
+
return code.startsWith("08");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class UnknownDbError extends DbError {
|
|
167
|
+
code;
|
|
168
|
+
table;
|
|
169
|
+
query;
|
|
170
|
+
constructor(code, message, table, query) {
|
|
171
|
+
super(message);
|
|
172
|
+
this.code = code;
|
|
173
|
+
this.table = table;
|
|
174
|
+
this.query = query;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function parsePgError(pgError, query) {
|
|
178
|
+
const { code, message, table, column, constraint, detail } = pgError;
|
|
179
|
+
switch (code) {
|
|
180
|
+
case "23505": {
|
|
181
|
+
const keyDetail = extractKeyDetail(detail);
|
|
182
|
+
return new UniqueConstraintError({
|
|
183
|
+
table: table ?? "unknown",
|
|
184
|
+
column: column ?? keyDetail?.column ?? "unknown",
|
|
185
|
+
value: keyDetail?.value,
|
|
186
|
+
query
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
case "23503": {
|
|
190
|
+
return new ForeignKeyError({
|
|
191
|
+
table: table ?? "unknown",
|
|
192
|
+
constraint: constraint ?? "unknown",
|
|
193
|
+
detail,
|
|
194
|
+
query
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
case "23502": {
|
|
198
|
+
const extractedColumn = extractNotNullColumn(message);
|
|
199
|
+
return new NotNullError({
|
|
200
|
+
table: table ?? "unknown",
|
|
201
|
+
column: column ?? extractedColumn ?? "unknown",
|
|
202
|
+
query
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
case "23514": {
|
|
206
|
+
const extractedConstraint = extractCheckConstraint(message);
|
|
207
|
+
return new CheckConstraintError({
|
|
208
|
+
table: table ?? "unknown",
|
|
209
|
+
constraint: constraint ?? extractedConstraint ?? "unknown",
|
|
210
|
+
query
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
default: {
|
|
214
|
+
if (isConnectionErrorCode(code)) {
|
|
215
|
+
return new ConnectionError(message);
|
|
216
|
+
}
|
|
217
|
+
return new UnknownDbError(code, message, table, query);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/query/executor.ts
|
|
223
|
+
function isPgError(error) {
|
|
224
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string";
|
|
225
|
+
}
|
|
226
|
+
async function executeQuery(queryFn, sql, params) {
|
|
227
|
+
try {
|
|
228
|
+
return await queryFn(sql, params);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (isPgError(error)) {
|
|
231
|
+
throw parsePgError(error, sql);
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/query/helpers.ts
|
|
238
|
+
function getColumnNames(table) {
|
|
239
|
+
return Object.keys(table._columns);
|
|
240
|
+
}
|
|
241
|
+
function getDefaultColumns(table) {
|
|
242
|
+
return Object.keys(table._columns).filter((key) => {
|
|
243
|
+
const col = table._columns[key];
|
|
244
|
+
return col ? !col._meta.hidden : true;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function getNotSensitiveColumns(table) {
|
|
248
|
+
return Object.keys(table._columns).filter((key) => {
|
|
249
|
+
const col = table._columns[key];
|
|
250
|
+
return col ? !col._meta.sensitive && !col._meta.hidden : true;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function getNotHiddenColumns(table) {
|
|
254
|
+
return Object.keys(table._columns).filter((key) => {
|
|
255
|
+
const col = table._columns[key];
|
|
256
|
+
return col ? !col._meta.hidden : true;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function resolveSelectColumns(table, select) {
|
|
260
|
+
if (!select) {
|
|
261
|
+
return getDefaultColumns(table);
|
|
262
|
+
}
|
|
263
|
+
if ("not" in select && select.not !== undefined) {
|
|
264
|
+
if (select.not === "sensitive") {
|
|
265
|
+
return getNotSensitiveColumns(table);
|
|
266
|
+
}
|
|
267
|
+
return getNotHiddenColumns(table);
|
|
268
|
+
}
|
|
269
|
+
return Object.keys(select).filter((k) => select[k] === true);
|
|
270
|
+
}
|
|
271
|
+
function getTimestampColumns(table) {
|
|
272
|
+
return Object.keys(table._columns).filter((key) => {
|
|
273
|
+
const col = table._columns[key];
|
|
274
|
+
return col ? col._meta.sqlType === "timestamp with time zone" : false;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function getPrimaryKeyColumns(table) {
|
|
278
|
+
return Object.keys(table._columns).filter((key) => {
|
|
279
|
+
const col = table._columns[key];
|
|
280
|
+
return col ? col._meta.primary : false;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/query/row-mapper.ts
|
|
285
|
+
function mapRow(row) {
|
|
286
|
+
const result = {};
|
|
287
|
+
for (const key of Object.keys(row)) {
|
|
288
|
+
result[snakeToCamel(key)] = row[key];
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
function mapRows(rows) {
|
|
293
|
+
return rows.map((row) => mapRow(row));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { DbError, UniqueConstraintError, ForeignKeyError, NotNullError, CheckConstraintError, NotFoundError, ConnectionError, ConnectionPoolExhaustedError, parsePgError, executeQuery, getColumnNames, getDefaultColumns, getNotSensitiveColumns, getNotHiddenColumns, resolveSelectColumns, getTimestampColumns, getPrimaryKeyColumns, mapRow, mapRows };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Casing conversion utilities for camelCase <-> snake_case.
|
|
3
|
+
*
|
|
4
|
+
* Used by SQL builders to convert JavaScript property names (camelCase)
|
|
5
|
+
* to PostgreSQL column names (snake_case) and vice versa.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Convert a camelCase string to snake_case.
|
|
9
|
+
*
|
|
10
|
+
* Handles acronyms correctly:
|
|
11
|
+
* - "parseJSON" -> "parse_json"
|
|
12
|
+
* - "getHTTPSUrl" -> "get_https_url"
|
|
13
|
+
* - "htmlParser" -> "html_parser"
|
|
14
|
+
*/
|
|
15
|
+
declare function camelToSnake(str: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Convert a snake_case string to camelCase.
|
|
18
|
+
*
|
|
19
|
+
* - "first_name" -> "firstName"
|
|
20
|
+
* - "created_at_timestamp" -> "createdAtTimestamp"
|
|
21
|
+
*/
|
|
22
|
+
declare function snakeToCamel(str: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* DELETE statement builder.
|
|
25
|
+
*
|
|
26
|
+
* Generates parameterized DELETE queries with support for:
|
|
27
|
+
* - WHERE clause via the where builder
|
|
28
|
+
* - RETURNING clause with column aliasing
|
|
29
|
+
* - camelCase -> snake_case column conversion
|
|
30
|
+
*/
|
|
31
|
+
interface DeleteOptions {
|
|
32
|
+
readonly table: string;
|
|
33
|
+
readonly where?: Record<string, unknown>;
|
|
34
|
+
readonly returning?: "*" | readonly string[];
|
|
35
|
+
}
|
|
36
|
+
interface DeleteResult {
|
|
37
|
+
readonly sql: string;
|
|
38
|
+
readonly params: readonly unknown[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a DELETE statement from the given options.
|
|
42
|
+
*/
|
|
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
|
+
*/
|
|
54
|
+
interface OnConflictOptions {
|
|
55
|
+
readonly columns: readonly string[];
|
|
56
|
+
readonly action: "nothing" | "update";
|
|
57
|
+
readonly updateColumns?: readonly string[];
|
|
58
|
+
/** Explicit update values for ON CONFLICT DO UPDATE SET (used by upsert). */
|
|
59
|
+
readonly updateValues?: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
interface InsertOptions {
|
|
62
|
+
readonly table: string;
|
|
63
|
+
readonly data: Record<string, unknown> | readonly Record<string, unknown>[];
|
|
64
|
+
readonly returning?: "*" | readonly string[];
|
|
65
|
+
readonly onConflict?: OnConflictOptions;
|
|
66
|
+
/** Column names (camelCase) that should use NOW() instead of a parameterized value when the value is "now". */
|
|
67
|
+
readonly nowColumns?: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
interface InsertResult {
|
|
70
|
+
readonly sql: string;
|
|
71
|
+
readonly params: readonly unknown[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build an INSERT statement from the given options.
|
|
75
|
+
*/
|
|
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
|
+
*/
|
|
88
|
+
interface SelectOptions {
|
|
89
|
+
readonly table: string;
|
|
90
|
+
readonly columns?: readonly string[];
|
|
91
|
+
readonly where?: Record<string, unknown>;
|
|
92
|
+
readonly orderBy?: Record<string, "asc" | "desc">;
|
|
93
|
+
readonly limit?: number;
|
|
94
|
+
readonly offset?: number;
|
|
95
|
+
readonly withCount?: boolean;
|
|
96
|
+
/** Cursor object: column-value pairs marking the position to paginate from. */
|
|
97
|
+
readonly cursor?: Record<string, unknown>;
|
|
98
|
+
/** Number of rows to take (used with cursor). Aliases `limit` when cursor is present. */
|
|
99
|
+
readonly take?: number;
|
|
100
|
+
}
|
|
101
|
+
interface SelectResult {
|
|
102
|
+
readonly sql: string;
|
|
103
|
+
readonly params: readonly unknown[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a SELECT statement from the given options.
|
|
107
|
+
*/
|
|
108
|
+
declare function buildSelect(options: SelectOptions): SelectResult;
|
|
109
|
+
/**
|
|
110
|
+
* SQL tagged template literal and escape hatch.
|
|
111
|
+
*
|
|
112
|
+
* Provides a safe, composable way to write raw SQL with automatic parameterization.
|
|
113
|
+
*
|
|
114
|
+
* Usage:
|
|
115
|
+
* const result = sql`SELECT * FROM users WHERE id = ${userId}`;
|
|
116
|
+
* // { sql: 'SELECT * FROM users WHERE id = $1', params: [userId] }
|
|
117
|
+
*
|
|
118
|
+
* const col = sql.raw('created_at');
|
|
119
|
+
* const result = sql`SELECT ${col} FROM users`;
|
|
120
|
+
* // { sql: 'SELECT created_at FROM users', params: [] }
|
|
121
|
+
*
|
|
122
|
+
* const where = sql`WHERE active = ${true}`;
|
|
123
|
+
* const query = sql`SELECT * FROM users ${where}`;
|
|
124
|
+
* // { sql: 'SELECT * FROM users WHERE active = $1', params: [true] }
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* A fragment of SQL with parameterized values.
|
|
128
|
+
*
|
|
129
|
+
* The `_tag` property is used to identify SqlFragment instances during
|
|
130
|
+
* template composition, distinguishing them from regular interpolated values.
|
|
131
|
+
*/
|
|
132
|
+
interface SqlFragment {
|
|
133
|
+
readonly _tag: "SqlFragment";
|
|
134
|
+
readonly sql: string;
|
|
135
|
+
readonly params: readonly unknown[];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* SQL tagged template with escape hatch.
|
|
139
|
+
*
|
|
140
|
+
* Use as a tagged template literal for automatic parameterization:
|
|
141
|
+
* sql`SELECT * FROM users WHERE id = ${userId}`
|
|
142
|
+
*
|
|
143
|
+
* Use sql.raw() for trusted raw SQL injection:
|
|
144
|
+
* sql.raw('column_name')
|
|
145
|
+
*
|
|
146
|
+
* Compose fragments by nesting sql`` inside sql``:
|
|
147
|
+
* const where = sql`WHERE active = ${true}`;
|
|
148
|
+
* const query = sql`SELECT * FROM users ${where}`;
|
|
149
|
+
*/
|
|
150
|
+
declare const sql: {
|
|
151
|
+
(strings: TemplateStringsArray, ...values: unknown[]): SqlFragment;
|
|
152
|
+
raw(value: string): SqlFragment;
|
|
153
|
+
};
|
|
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
|
+
interface UpdateOptions {
|
|
165
|
+
readonly table: string;
|
|
166
|
+
readonly data: Record<string, unknown>;
|
|
167
|
+
readonly where?: Record<string, unknown>;
|
|
168
|
+
readonly returning?: "*" | readonly string[];
|
|
169
|
+
/** Column names (camelCase) that should use NOW() instead of a parameterized value when the value is "now". */
|
|
170
|
+
readonly nowColumns?: readonly string[];
|
|
171
|
+
}
|
|
172
|
+
interface UpdateResult {
|
|
173
|
+
readonly sql: string;
|
|
174
|
+
readonly params: readonly unknown[];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Build an UPDATE statement from the given options.
|
|
178
|
+
*/
|
|
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
|
+
*/
|
|
195
|
+
interface WhereResult {
|
|
196
|
+
readonly sql: string;
|
|
197
|
+
readonly params: readonly unknown[];
|
|
198
|
+
}
|
|
199
|
+
interface WhereFilter {
|
|
200
|
+
readonly [key: string]: unknown;
|
|
201
|
+
readonly OR?: readonly WhereFilter[];
|
|
202
|
+
readonly AND?: readonly WhereFilter[];
|
|
203
|
+
readonly NOT?: WhereFilter;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Build a WHERE clause from a filter object.
|
|
207
|
+
*
|
|
208
|
+
* @param filter - The filter object with column conditions
|
|
209
|
+
* @param paramOffset - Starting parameter offset (0-based, params start at $offset+1)
|
|
210
|
+
* @returns WhereResult with the SQL string (without WHERE keyword) and parameter values
|
|
211
|
+
*/
|
|
212
|
+
declare function buildWhere(filter: WhereFilter | undefined, paramOffset?: number): WhereResult;
|
|
213
|
+
export { sql, snakeToCamel, camelToSnake, buildWhere, buildUpdate, buildSelect, buildInsert, buildDelete, WhereResult, UpdateResult, UpdateOptions, SqlFragment, SelectResult, SelectOptions, OnConflictOptions, InsertResult, InsertOptions, DeleteResult, DeleteOptions };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDelete,
|
|
3
|
+
buildInsert,
|
|
4
|
+
buildSelect,
|
|
5
|
+
buildUpdate,
|
|
6
|
+
buildWhere
|
|
7
|
+
} from "../shared/chunk-3f2grpak.js";
|
|
8
|
+
import {
|
|
9
|
+
camelToSnake,
|
|
10
|
+
snakeToCamel
|
|
11
|
+
} from "../shared/chunk-hrfdj0rr.js";
|
|
12
|
+
// src/sql/tagged.ts
|
|
13
|
+
function isSqlFragment(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && "_tag" in value && value._tag === "SqlFragment";
|
|
15
|
+
}
|
|
16
|
+
function renumberParams(sqlStr, offset) {
|
|
17
|
+
let counter = 0;
|
|
18
|
+
return sqlStr.replace(/\$(\d+)/g, () => {
|
|
19
|
+
counter++;
|
|
20
|
+
return `$${offset + counter}`;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function sqlTag(strings, ...values) {
|
|
24
|
+
const sqlParts = [];
|
|
25
|
+
const allParams = [];
|
|
26
|
+
for (let i = 0;i < strings.length; i++) {
|
|
27
|
+
const part = strings[i] ?? "";
|
|
28
|
+
sqlParts.push(part);
|
|
29
|
+
if (i < values.length) {
|
|
30
|
+
const value = values[i];
|
|
31
|
+
if (isSqlFragment(value)) {
|
|
32
|
+
const renumbered = renumberParams(value.sql, allParams.length);
|
|
33
|
+
sqlParts.push(renumbered);
|
|
34
|
+
allParams.push(...value.params);
|
|
35
|
+
} else {
|
|
36
|
+
allParams.push(value);
|
|
37
|
+
sqlParts.push(`$${allParams.length}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
_tag: "SqlFragment",
|
|
43
|
+
sql: sqlParts.join(""),
|
|
44
|
+
params: allParams
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function rawSql(value) {
|
|
48
|
+
return {
|
|
49
|
+
_tag: "SqlFragment",
|
|
50
|
+
sql: value,
|
|
51
|
+
params: []
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
var sql = Object.assign(sqlTag, { raw: rawSql });
|
|
55
|
+
export {
|
|
56
|
+
sql,
|
|
57
|
+
snakeToCamel,
|
|
58
|
+
camelToSnake,
|
|
59
|
+
buildWhere,
|
|
60
|
+
buildUpdate,
|
|
61
|
+
buildSelect,
|
|
62
|
+
buildInsert,
|
|
63
|
+
buildDelete
|
|
64
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vertz/db",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Database layer for Vertz — typed queries, migrations, codegen",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/vertz-dev/vertz.git",
|
|
10
|
+
"directory": "packages/db"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public",
|
|
14
|
+
"provenance": true
|
|
15
|
+
},
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./sql": {
|
|
24
|
+
"types": "./dist/sql/index.d.ts",
|
|
25
|
+
"import": "./dist/sql/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./internals": {
|
|
28
|
+
"types": "./dist/internals.d.ts",
|
|
29
|
+
"import": "./dist/internals.js"
|
|
30
|
+
},
|
|
31
|
+
"./plugin": {
|
|
32
|
+
"types": "./dist/plugin/index.d.ts",
|
|
33
|
+
"import": "./dist/plugin/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./diagnostic": {
|
|
36
|
+
"types": "./dist/diagnostic/index.d.ts",
|
|
37
|
+
"import": "./dist/diagnostic/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./core": {
|
|
40
|
+
"types": "./dist/core/index.d.ts",
|
|
41
|
+
"import": "./dist/core/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./schema-derive": {
|
|
44
|
+
"types": "./dist/schema-derive/index.d.ts",
|
|
45
|
+
"import": "./dist/schema-derive/index.js"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist"
|
|
50
|
+
],
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "bunup",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:watch": "vitest",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
59
|
+
"@types/node": "^25.2.1",
|
|
60
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
61
|
+
"bunup": "latest",
|
|
62
|
+
"typescript": "^5.7.0",
|
|
63
|
+
"vitest": "^4.0.18"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@vertz/schema": "workspace:*",
|
|
70
|
+
"postgres": "^3.4.8"
|
|
71
|
+
}
|
|
72
|
+
}
|