@vertz/db 0.2.15 → 0.2.16

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.
@@ -160,7 +160,9 @@ function buildFilterClauses(filter, paramOffset, overrides, dialect) {
160
160
  continue;
161
161
  }
162
162
  const columnRef = resolveColumnRef(key, overrides, dialect);
163
- if (isOperatorObject(value)) {
163
+ if (value === null) {
164
+ clauses.push(`${columnRef} IS NULL`);
165
+ } else if (isOperatorObject(value)) {
164
166
  const result = buildOperatorCondition(columnRef, value, idx, dialect);
165
167
  clauses.push(...result.clauses);
166
168
  allParams.push(...result.params);
@@ -372,7 +374,10 @@ function buildSelect(options, dialect = options.dialect ?? defaultPostgresDialec
372
374
  parts.push(`WHERE ${whereClauses.join(" AND ")}`);
373
375
  }
374
376
  if (options.orderBy) {
375
- const orderClauses = Object.entries(options.orderBy).map(([col, dir]) => `"${camelToSnake(col, casingOverrides)}" ${dir.toUpperCase()}`);
377
+ const orderClauses = Object.entries(options.orderBy).map(([col, dir]) => {
378
+ const safeDir = dir.toUpperCase() === "DESC" ? "DESC" : "ASC";
379
+ return `"${camelToSnake(col, casingOverrides)}" ${safeDir}`;
380
+ });
376
381
  if (orderClauses.length > 0) {
377
382
  parts.push(`ORDER BY ${orderClauses.join(", ")}`);
378
383
  }
@@ -122,10 +122,14 @@ function buildWhereClause(where, columns) {
122
122
  const clauses = [];
123
123
  const params = [];
124
124
  for (const [key, value] of Object.entries(where)) {
125
- clauses.push(`${key} = ?`);
126
- const colMeta = columns[key]?._meta;
127
- const convertedValue = convertValueForSql(value, colMeta?.sqlType);
128
- params.push(convertedValue);
125
+ if (value === null) {
126
+ clauses.push(`${key} IS NULL`);
127
+ } else {
128
+ clauses.push(`${key} = ?`);
129
+ const colMeta = columns[key]?._meta;
130
+ const convertedValue = convertValueForSql(value, colMeta?.sqlType);
131
+ params.push(convertedValue);
132
+ }
129
133
  }
130
134
  return { clauses, params };
131
135
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createPostgresDriver
3
- } from "./chunk-rqe0prft.js";
3
+ } from "./chunk-rjry8vc2.js";
4
4
  import"./chunk-j4kwq1gh.js";
5
5
  export {
6
6
  createPostgresDriver
@@ -63,6 +63,29 @@ function createPostgresDriver(url, pool) {
63
63
  const result = await queryFn(sql2, params ?? []);
64
64
  return { rowsAffected: result.rowCount };
65
65
  },
66
+ async beginTransaction(fn) {
67
+ return sql.begin(async (txSql) => {
68
+ const txQueryFn = async (sqlStr, params) => {
69
+ try {
70
+ const result = await txSql.unsafe(sqlStr, params);
71
+ const rows = result.map((row) => {
72
+ const mapped = {};
73
+ for (const [key, value] of Object.entries(row)) {
74
+ mapped[key] = coerceValue(value);
75
+ }
76
+ return mapped;
77
+ });
78
+ return {
79
+ rows,
80
+ rowCount: result.count ?? rows.length
81
+ };
82
+ } catch (error) {
83
+ adaptPostgresError(error);
84
+ }
85
+ };
86
+ return fn(txQueryFn);
87
+ });
88
+ },
66
89
  async close() {
67
90
  await sql.end();
68
91
  },
@@ -2,7 +2,7 @@ import {
2
2
  BaseSqlAdapter,
3
3
  generateCreateTableSql,
4
4
  generateIndexSql
5
- } from "./chunk-pkv8w501.js";
5
+ } from "./chunk-ed82s3sa.js";
6
6
 
7
7
  // src/adapters/d1-adapter.ts
8
8
  function createD1Driver(d1) {
package/dist/sql/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  buildSelect,
5
5
  buildUpdate,
6
6
  buildWhere
7
- } from "../shared/chunk-pxjcpnpx.js";
7
+ } from "../shared/chunk-3a9nybt2.js";
8
8
  import {
9
9
  camelToSnake,
10
10
  snakeToCamel
@@ -1,4 +1,17 @@
1
1
  /**
2
+ * Query executor — wraps raw SQL execution with error mapping.
3
+ *
4
+ * Takes a query function (from the database driver) and wraps it to:
5
+ * 1. Execute parameterized SQL
6
+ * 2. Map PG errors to typed DbError subclasses
7
+ * 3. Return typed QueryResult
8
+ */
9
+ interface ExecutorResult<T> {
10
+ readonly rows: readonly T[];
11
+ readonly rowCount: number;
12
+ }
13
+ type QueryFn = <T>(sql: string, params: readonly unknown[]) => Promise<ExecutorResult<T>>;
14
+ /**
2
15
  * Database driver interface.
3
16
  *
4
17
  * Provides a unified interface for different database backends
@@ -16,6 +29,12 @@ interface DbDriver {
16
29
  rowsAffected: number;
17
30
  }>;
18
31
  /**
32
+ * Execute a callback within a database transaction.
33
+ * The callback receives a transaction-scoped QueryFn.
34
+ * Optional — not all drivers support transactions (e.g., D1).
35
+ */
36
+ beginTransaction?<T>(fn: (txQueryFn: QueryFn) => Promise<T>): Promise<T>;
37
+ /**
19
38
  * Close the database connection.
20
39
  */
21
40
  close(): Promise<void>;
@@ -85,6 +104,20 @@ interface ColumnBuilder<
85
104
  }>;
86
105
  }
87
106
  type InferColumnType<C> = C extends ColumnBuilder<infer T, ColumnMetadata> ? T : never;
107
+ interface ThroughDef<TJoin extends TableDef<ColumnRecord> = TableDef<ColumnRecord>> {
108
+ readonly table: () => TJoin;
109
+ readonly thisKey: string;
110
+ readonly thatKey: string;
111
+ }
112
+ interface RelationDef<
113
+ TTarget extends TableDef<ColumnRecord> = TableDef<ColumnRecord>,
114
+ TType extends "one" | "many" = "one" | "many"
115
+ > {
116
+ readonly _type: TType;
117
+ readonly _target: () => TTarget;
118
+ readonly _foreignKey: string | null;
119
+ readonly _through: ThroughDef | null;
120
+ }
88
121
  type IndexType = "btree" | "hash" | "gin" | "gist" | "brin";
89
122
  interface IndexDef {
90
123
  readonly columns: readonly string[];
@@ -173,28 +206,129 @@ interface TableDef<TColumns extends ColumnRecord = ColumnRecord> {
173
206
  /** Mark this table as shared / cross-tenant. */
174
207
  shared(): TableDef<TColumns>;
175
208
  }
209
+ /** Operators available for comparable types (number, string, Date, bigint). */
210
+ interface ComparisonOperators<T> {
211
+ readonly eq?: T;
212
+ readonly ne?: T;
213
+ readonly gt?: T;
214
+ readonly gte?: T;
215
+ readonly lt?: T;
216
+ readonly lte?: T;
217
+ readonly in?: readonly T[];
218
+ readonly notIn?: readonly T[];
219
+ }
220
+ /** Additional operators for string columns. */
221
+ interface StringOperators {
222
+ readonly contains?: string;
223
+ readonly startsWith?: string;
224
+ readonly endsWith?: string;
225
+ }
226
+ /** The `isNull` operator — only available for nullable columns. */
227
+ interface NullOperator {
228
+ readonly isNull?: boolean;
229
+ }
176
230
  /**
177
- * Database Adapter Types for @vertz/db
231
+ * Resolves the filter operators for a single column based on its inferred type
232
+ * and nullable metadata.
178
233
  *
179
- * Generic adapter interface that abstracts database operations.
180
- * Implemented by SQLite, D1, and other database adapters.
234
+ * - All types get comparison + in/notIn
235
+ * - String types additionally get contains, startsWith, endsWith
236
+ * - Nullable columns additionally get isNull
237
+ *
238
+ * Uses [T] extends [string] to prevent union distribution -- ensures that a
239
+ * union like 'admin' | 'editor' keeps the full union in each operator slot.
181
240
  */
182
- interface ListOptions {
241
+ type ColumnFilterOperators<
242
+ TType,
243
+ TNullable extends boolean
244
+ > = ([TType] extends [string] ? ComparisonOperators<TType> & StringOperators : ComparisonOperators<TType>) & (TNullable extends true ? NullOperator : unknown);
245
+ /** Determine whether a column is nullable from its metadata. */
246
+ type IsNullable<C> = C extends ColumnBuilder<unknown, infer M> ? M extends {
247
+ readonly nullable: true;
248
+ } ? true : false : false;
249
+ /**
250
+ * FilterType<TColumns> — typed where clause.
251
+ *
252
+ * Each key maps to either:
253
+ * - A direct value (shorthand for `{ eq: value }`)
254
+ * - An object with typed filter operators
255
+ */
256
+ type FilterType<TColumns extends ColumnRecord> = { [K in keyof TColumns]? : InferColumnType<TColumns[K]> | ColumnFilterOperators<InferColumnType<TColumns[K]>, IsNullable<TColumns[K]>> };
257
+ type OrderByType<TColumns extends ColumnRecord> = { [K in keyof TColumns]? : "asc" | "desc" };
258
+ /** Relations record — maps relation names to RelationDef. */
259
+ type RelationsRecord = Record<string, RelationDef>;
260
+ /**
261
+ * The shape of include options for a given relations record.
262
+ * Each relation can be:
263
+ * - `true` — include with default fields
264
+ * - An object with `select`, `where`, `orderBy`, `limit` constrained to target table columns
265
+ */
266
+ type IncludeOption<TRelations extends RelationsRecord> = { [K in keyof TRelations]? : true | (RelationTarget<TRelations[K]> extends TableDef<infer TCols> ? {
267
+ select?: { [C in keyof TCols]? : true };
268
+ where?: FilterType<TCols>;
269
+ orderBy?: OrderByType<TCols>;
270
+ limit?: number;
271
+ /** Nested includes — untyped until full model registry is threaded through. */
272
+ include?: Record<string, unknown>;
273
+ } : never) };
274
+ /** Extract the target table from a RelationDef. */
275
+ type RelationTarget<R> = R extends RelationDef<infer TTarget, "one" | "many"> ? TTarget : never;
276
+ /** A model entry in the database registry, pairing a table with its relations. */
277
+ interface ModelEntry<
278
+ TTable extends TableDef<ColumnRecord> = TableDef<ColumnRecord>,
279
+ TRelations extends RelationsRecord = RelationsRecord
280
+ > {
281
+ readonly table: TTable;
282
+ readonly relations: TRelations;
283
+ }
284
+ /** A single include entry with optional query constraints. */
285
+ interface AdapterIncludeEntry {
286
+ select?: Record<string, true>;
183
287
  where?: Record<string, unknown>;
184
288
  orderBy?: Record<string, "asc" | "desc">;
185
289
  limit?: number;
290
+ include?: Record<string, unknown>;
291
+ }
292
+ /** Include specification: maps relation names to `true` or structured entries. */
293
+ type AdapterIncludeSpec = Record<string, true | AdapterIncludeEntry>;
294
+ /**
295
+ * Resolves the where clause type for a given entry.
296
+ * When TEntry is the default (unparameterized), falls back to Record<string, unknown>.
297
+ */
298
+ type ResolveWhere<TEntry extends ModelEntry> = TEntry extends ModelEntry<infer TTable> ? TTable extends TableDef<infer TCols> ? [ColumnRecord] extends [TCols] ? Record<string, unknown> : FilterType<TCols> : Record<string, unknown> : Record<string, unknown>;
299
+ /**
300
+ * Resolves the orderBy type for a given entry.
301
+ * When TEntry is the default (unparameterized), falls back to Record<string, 'asc' | 'desc'>.
302
+ */
303
+ type ResolveOrderBy<TEntry extends ModelEntry> = TEntry extends ModelEntry<infer TTable> ? TTable extends TableDef<infer TCols> ? [ColumnRecord] extends [TCols] ? Record<string, "asc" | "desc"> : OrderByType<TCols> : Record<string, "asc" | "desc"> : Record<string, "asc" | "desc">;
304
+ /**
305
+ * Resolves the include type for a given entry.
306
+ * When TEntry is the default (unparameterized), falls back to AdapterIncludeSpec.
307
+ */
308
+ type ResolveInclude<TEntry extends ModelEntry> = TEntry extends ModelEntry<TableDef<ColumnRecord>, infer TRels> ? [Record<string, never>] extends [TRels] ? AdapterIncludeSpec : IncludeOption<TRels> : AdapterIncludeSpec;
309
+ interface ListOptions<TEntry extends ModelEntry = ModelEntry> {
310
+ where?: ResolveWhere<TEntry>;
311
+ orderBy?: ResolveOrderBy<TEntry>;
312
+ limit?: number;
186
313
  /** Cursor-based pagination: fetch records after this ID. */
187
314
  after?: string;
315
+ /** Relation include specification for relation loading. */
316
+ include?: ResolveInclude<TEntry>;
317
+ }
318
+ /** Options for get-by-id operations. */
319
+ interface GetOptions<TEntry extends ModelEntry = ModelEntry> {
320
+ /** Relation include specification for relation loading. */
321
+ include?: ResolveInclude<TEntry>;
188
322
  }
189
- interface EntityDbAdapter {
190
- get(id: string): Promise<Record<string, unknown> | null>;
191
- list(options?: ListOptions): Promise<{
192
- data: Record<string, unknown>[];
323
+ interface EntityDbAdapter<TEntry extends ModelEntry = ModelEntry> {
324
+ get(id: string, options?: GetOptions<TEntry>): Promise<TEntry["table"]["$response"] | null>;
325
+ list(options?: ListOptions<TEntry>): Promise<{
326
+ data: TEntry["table"]["$response"][];
193
327
  total: number;
194
328
  }>;
195
- create(data: Record<string, unknown>): Promise<Record<string, unknown>>;
196
- update(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
197
- delete(id: string): Promise<Record<string, unknown> | null>;
329
+ create(data: TEntry["table"]["$create_input"]): Promise<TEntry["table"]["$response"]>;
330
+ update(id: string, data: TEntry["table"]["$update_input"]): Promise<TEntry["table"]["$response"]>;
331
+ delete(id: string): Promise<TEntry["table"]["$response"] | null>;
198
332
  }
199
333
  interface SqliteAdapterOptions<T extends ColumnRecord> {
200
334
  /** The table schema definition */
@@ -2,7 +2,7 @@ import {
2
2
  BaseSqlAdapter,
3
3
  generateCreateTableSql,
4
4
  generateIndexSql
5
- } from "../shared/chunk-pkv8w501.js";
5
+ } from "../shared/chunk-ed82s3sa.js";
6
6
  import {
7
7
  __commonJS,
8
8
  __require
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/db",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Database layer for Vertz — typed queries, migrations, codegen",
@@ -81,8 +81,8 @@
81
81
  "node": ">=22"
82
82
  },
83
83
  "dependencies": {
84
- "@vertz/errors": "^0.2.14",
85
- "@vertz/schema": "^0.2.14",
84
+ "@vertz/errors": "^0.2.15",
85
+ "@vertz/schema": "^0.2.15",
86
86
  "@paralleldrive/cuid2": "^3.3.0",
87
87
  "nanoid": "^5.1.5",
88
88
  "uuid": "^13.0.0"