@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.
@@ -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
+ }