@nextlyhq/adapter-drizzle 0.0.2-alpha.60 → 0.0.2-alpha.64

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/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- export { D as DrizzleAdapter } from './adapter-BG7MnOUw.cjs';
1
+ export { D as DrizzleAdapter, T as TransactionCrudDelegator, a as TransactionCrudForwarders } from './adapter-C6ENLDau.cjs';
2
2
  export { T as TableResolver } from './core-CVO7WYDj.cjs';
3
3
  import 'drizzle-orm';
4
- import './migration-BUc56kip.cjs';
4
+ import './migration-B6AmjCQ1.cjs';
5
5
  import './schema-BDn8WfSL.cjs';
6
6
  import './error-BrdknH2s.cjs';
7
7
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { D as DrizzleAdapter } from './adapter-DjPoLjBK.js';
1
+ export { D as DrizzleAdapter, T as TransactionCrudDelegator, a as TransactionCrudForwarders } from './adapter-D4GM1VAZ.js';
2
2
  export { T as TableResolver } from './core-CVO7WYDj.js';
3
3
  import 'drizzle-orm';
4
- import './migration-Bj84e15B.js';
4
+ import './migration-R06fsTrz.js';
5
5
  import './schema-BIQ0YQZ_.js';
6
6
  import './error-BrdknH2s.js';
7
7
 
package/dist/index.mjs CHANGED
@@ -1,6 +1,17 @@
1
- import { getColumns, desc, asc, and, or, not, like, notBetween, between, isNotNull, isNull, notInArray, inArray, ilike, lte, gte, lt, gt, ne, eq } from 'drizzle-orm';
1
+ import { getColumns, count, desc, asc, sql, and, or, not, notBetween, between, isNotNull, isNull, notInArray, inArray, ilike, like, lte, gte, lt, gt, ne, eq } from 'drizzle-orm';
2
2
 
3
3
  // src/adapter.ts
4
+ function buildDrizzleOrderBy(columns, orderBy) {
5
+ if (!orderBy?.length) return [];
6
+ return orderBy.flatMap((spec) => {
7
+ const col = columns[spec.column];
8
+ if (!col) return [];
9
+ const direction = spec.direction === "desc" ? desc(col) : asc(col);
10
+ if (spec.nulls === void 0) return [direction];
11
+ const isNull2 = sql`${col} is null`;
12
+ return [spec.nulls === "last" ? asc(isNull2) : desc(isNull2), direction];
13
+ });
14
+ }
4
15
  function buildDrizzleWhere(table, where) {
5
16
  const columns = getColumns(table);
6
17
  return processWhereClause(columns, where);
@@ -36,13 +47,26 @@ function processWhereClause(columns, where) {
36
47
  parts.push(not(notCondition));
37
48
  }
38
49
  }
39
- if (parts.length === 0) return void 0;
50
+ if (parts.length === 0) {
51
+ if (namesABranch(where)) {
52
+ throw new Error(
53
+ `Where clause produced no condition: ${JSON.stringify(where)}. Every branch resolved to nothing, which would match every row. Pass {} to mean "no filter".`
54
+ );
55
+ }
56
+ return void 0;
57
+ }
40
58
  if (parts.length === 1) return parts[0];
41
59
  return and(...parts);
42
60
  }
43
61
  function isWhereCondition(item) {
44
62
  return "column" in item && "op" in item;
45
63
  }
64
+ function namesABranch(where) {
65
+ return Boolean(where.and?.length || where.or?.length || where.not);
66
+ }
67
+ function containsPattern(value) {
68
+ return `%${value.replace(/[!%_]/g, (character) => `!${character}`)}%`;
69
+ }
46
70
  function buildCondition(columns, cond) {
47
71
  const column = columns[cond.column];
48
72
  if (!column) {
@@ -81,12 +105,37 @@ function buildCondition(columns, cond) {
81
105
  case "NOT BETWEEN":
82
106
  return notBetween(col, cond.value, cond.valueTo);
83
107
  case "CONTAINS":
84
- return like(col, `%${String(cond.value)}%`);
108
+ return sql`${col} like ${containsPattern(String(cond.value))} escape '!'`;
85
109
  default:
86
110
  throw new Error(`Unsupported operator: ${cond.op}`);
87
111
  }
88
112
  }
89
113
 
114
+ // src/transaction-forwarders.ts
115
+ function createTransactionForwarders(delegator, txDb) {
116
+ return {
117
+ select: async (table, options) => {
118
+ return delegator.select(table, options, txDb());
119
+ },
120
+ selectOne: async (table, options) => {
121
+ return delegator.selectOne(table, options, txDb());
122
+ },
123
+ update: async (table, data, where, options) => {
124
+ return delegator.update(table, data, where, options, txDb());
125
+ },
126
+ updateCount: async (table, data, where) => {
127
+ return delegator.updateCount(table, data, where, txDb());
128
+ },
129
+ delete: async (table, where, options) => {
130
+ return delegator.delete(table, where, options, txDb());
131
+ },
132
+ upsert: async (table, data, options) => {
133
+ return delegator.upsert(table, data, options, txDb());
134
+ },
135
+ getDrizzle: () => txDb()
136
+ };
137
+ }
138
+
90
139
  // src/types/error.ts
91
140
  function isDatabaseError(error) {
92
141
  return typeof error === "object" && error !== null && "kind" in error && typeof error.kind === "string";
@@ -518,6 +567,23 @@ var DrizzleAdapter = class {
518
567
  if (names == null || names === "*" || !tableObj || typeof tableObj !== "object") {
519
568
  return void 0;
520
569
  }
570
+ const { projection } = this.resolveColumns(tableObj, names);
571
+ return Object.keys(projection).length ? projection : void 0;
572
+ }
573
+ /**
574
+ * The requested column names split into what this table has and what it does not.
575
+ *
576
+ * One resolution, reported both ways, because two callers need different halves
577
+ * of the same answer: a projection drops what it cannot find, while a distinct
578
+ * count has to REFUSE it. Resolving twice would let the two disagree about what
579
+ * "found" means — the SQL-name alias below is exactly the kind of detail a
580
+ * second implementation gets wrong — and the count would then reject a column
581
+ * the projection happily uses.
582
+ *
583
+ * Both spellings resolve: a caller may name the Drizzle property or the SQL
584
+ * column, and either maps to the same projection key.
585
+ */
586
+ resolveColumns(tableObj, names) {
521
587
  const cols = getColumns(tableObj);
522
588
  const byAnyName = {};
523
589
  for (const [jsName, col] of Object.entries(cols)) {
@@ -526,11 +592,13 @@ var DrizzleAdapter = class {
526
592
  if (typeof sqlName === "string") byAnyName[sqlName] = { jsName, col };
527
593
  }
528
594
  const projection = {};
595
+ const unresolved = [];
529
596
  for (const name of names) {
530
597
  const hit = byAnyName[name];
531
598
  if (hit) projection[hit.jsName] = hit.col;
599
+ else unresolved.push(name);
532
600
  }
533
- return Object.keys(projection).length ? projection : void 0;
601
+ return { projection, unresolved };
534
602
  }
535
603
  // ============================================================
536
604
  // Connection Status (Default implementations, can override)
@@ -702,12 +770,10 @@ var DrizzleAdapter = class {
702
770
  }
703
771
  }
704
772
  if (options?.orderBy?.length) {
705
- const columns = getColumns(tableObj);
706
- const orderClauses = options.orderBy.map((o) => {
707
- const col = columns[o.column];
708
- if (!col) return void 0;
709
- return o.direction === "desc" ? desc(col) : asc(col);
710
- }).filter(Boolean);
773
+ const orderClauses = buildDrizzleOrderBy(
774
+ getColumns(tableObj),
775
+ options.orderBy
776
+ );
711
777
  if (orderClauses.length) {
712
778
  query = query.orderBy(...orderClauses);
713
779
  }
@@ -759,6 +825,93 @@ var DrizzleAdapter = class {
759
825
  * });
760
826
  * ```
761
827
  */
828
+ /** The registered table object, or a refusal naming what is missing. */
829
+ requireTableObject(table) {
830
+ const tableObj = this.getTableObject(table);
831
+ if (tableObj) return tableObj;
832
+ throw this.createDatabaseError(
833
+ "query",
834
+ `Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
835
+ void 0
836
+ );
837
+ }
838
+ /**
839
+ * The columns a distinct count groups by, or `undefined` for a row count.
840
+ *
841
+ * 🔴 EVERY named column must resolve, not merely one of them. A `distinctOn`
842
+ * naming no column this table has would fall through to a row count, and one
843
+ * naming a valid column beside a misspelled or since-removed column would
844
+ * count distinct over a NARROWER key than the caller asked for. Both answer a
845
+ * different question than the one asked, silently; the second is the worse of
846
+ * the two, because a subset key groups more rows together and so UNDERCOUNTS,
847
+ * which reads as a plausible number rather than as a fault. Refused here,
848
+ * where the mistake was made, rather than surfacing as a wrong figure.
849
+ */
850
+ countProjection(tableObj, table, options) {
851
+ const distinctOn = options?.distinctOn;
852
+ if (!distinctOn?.length) return void 0;
853
+ const { projection, unresolved } = this.resolveColumns(
854
+ tableObj,
855
+ distinctOn
856
+ );
857
+ if (unresolved.length > 0) {
858
+ throw this.createDatabaseError(
859
+ "query",
860
+ `distinctOn names ${unresolved.length === 1 ? "a column" : "columns"} that do not exist on "${table}": ${unresolved.join(", ")}`,
861
+ void 0
862
+ );
863
+ }
864
+ return projection;
865
+ }
866
+ /**
867
+ * `COUNT(*)` over a `SELECT DISTINCT` subquery.
868
+ *
869
+ * Its own function because it is a genuinely different query from the plain
870
+ * count rather than a variation on one, and because building both inline made
871
+ * the public method a chain of decisions the complexity gate objected to
872
+ * before a reader would have.
873
+ */
874
+ async countDistinctRows(db, tableObj, projection, where) {
875
+ const inner = db.selectDistinct(projection).from(tableObj).$dynamic();
876
+ const distinctRows = where ? inner.where(where) : inner;
877
+ const [row] = await db.select({ value: count() }).from(distinctRows.as("nextly_distinct"));
878
+ return Number(row?.value ?? 0);
879
+ }
880
+ /** `COUNT(*)` over the table itself. */
881
+ async countAllRows(db, tableObj, where) {
882
+ const base = db.select({ value: count() }).from(tableObj).$dynamic();
883
+ const [row] = await (where ? base.where(where) : base);
884
+ return Number(row?.value ?? 0);
885
+ }
886
+ /**
887
+ * How many rows match, or how many DISTINCT combinations of some columns do.
888
+ *
889
+ * The data layer had no count at all: collection totals go through
890
+ * `countEntries`, an access-controlled path built for collection tables, and
891
+ * nothing could count a system table. A caller wanting one selected the rows
892
+ * and measured the array, which transfers every row to learn a number and
893
+ * cannot be bounded without making the number wrong.
894
+ *
895
+ * 🔴 `distinctOn` compiles to `COUNT(*)` over a `SELECT DISTINCT` SUBQUERY,
896
+ * never to `COUNT(DISTINCT a, b)`. The inline form is not portable and fails
897
+ * in the direction that is hardest to notice -- MySQL accepts it, PostgreSQL
898
+ * needs a row constructor, and SQLite rejects it outright with "wrong number
899
+ * of arguments to function count()" -- so a query written against one engine
900
+ * is a syntax error on another. The subquery is the single form all three
901
+ * accept, and building it here rather than at each call site is what keeps
902
+ * the answer identical per dialect.
903
+ */
904
+ async count(table, options, executor) {
905
+ const tableObj = this.requireTableObject(table);
906
+ try {
907
+ const db = executor ?? this.getDrizzle();
908
+ const where = options?.where ? buildDrizzleWhere(tableObj, options.where) : void 0;
909
+ const projection = this.countProjection(tableObj, table, options);
910
+ return projection ? await this.countDistinctRows(db, tableObj, projection, where) : await this.countAllRows(db, tableObj, where);
911
+ } catch (error) {
912
+ throw this.handleQueryError(error, "count", table);
913
+ }
914
+ }
762
915
  async selectOne(table, options, executor) {
763
916
  const results = await this.select(
764
917
  table,
@@ -1303,11 +1456,11 @@ var DrizzleAdapter = class {
1303
1456
  */
1304
1457
  async tableExists(tableName, schema) {
1305
1458
  try {
1306
- let sql;
1459
+ let sql3;
1307
1460
  const params = [];
1308
1461
  switch (this.dialect) {
1309
1462
  case "postgresql":
1310
- sql = `
1463
+ sql3 = `
1311
1464
  SELECT EXISTS (
1312
1465
  SELECT FROM information_schema.tables
1313
1466
  WHERE table_schema = COALESCE($1, (
@@ -1321,7 +1474,7 @@ var DrizzleAdapter = class {
1321
1474
  params.push(schema ?? null, tableName);
1322
1475
  break;
1323
1476
  case "mysql":
1324
- sql = `
1477
+ sql3 = `
1325
1478
  SELECT COUNT(*) as count
1326
1479
  FROM information_schema.tables
1327
1480
  WHERE table_schema = DATABASE()
@@ -1330,7 +1483,7 @@ var DrizzleAdapter = class {
1330
1483
  params.push(tableName);
1331
1484
  break;
1332
1485
  case "sqlite":
1333
- sql = `
1486
+ sql3 = `
1334
1487
  SELECT COUNT(*) as count
1335
1488
  FROM sqlite_master
1336
1489
  WHERE type = 'table'
@@ -1346,7 +1499,7 @@ var DrizzleAdapter = class {
1346
1499
  );
1347
1500
  }
1348
1501
  const results = await this.executeQuery(
1349
- sql,
1502
+ sql3,
1350
1503
  params
1351
1504
  );
1352
1505
  if (results.length === 0) {
@@ -1378,11 +1531,11 @@ var DrizzleAdapter = class {
1378
1531
  */
1379
1532
  async listTables(schema) {
1380
1533
  try {
1381
- let sql;
1534
+ let sql3;
1382
1535
  const params = [];
1383
1536
  switch (this.dialect) {
1384
1537
  case "postgresql":
1385
- sql = `
1538
+ sql3 = `
1386
1539
  SELECT table_name
1387
1540
  FROM information_schema.tables
1388
1541
  WHERE table_schema = $1
@@ -1392,7 +1545,7 @@ var DrizzleAdapter = class {
1392
1545
  params.push(schema ?? "public");
1393
1546
  break;
1394
1547
  case "mysql":
1395
- sql = `
1548
+ sql3 = `
1396
1549
  SELECT table_name AS table_name
1397
1550
  FROM information_schema.tables
1398
1551
  WHERE table_schema = DATABASE()
@@ -1401,7 +1554,7 @@ var DrizzleAdapter = class {
1401
1554
  `;
1402
1555
  break;
1403
1556
  case "sqlite":
1404
- sql = `
1557
+ sql3 = `
1405
1558
  SELECT name as table_name
1406
1559
  FROM sqlite_master
1407
1560
  WHERE type = 'table'
@@ -1417,7 +1570,7 @@ var DrizzleAdapter = class {
1417
1570
  );
1418
1571
  }
1419
1572
  const results = await this.executeQuery(
1420
- sql,
1573
+ sql3,
1421
1574
  params
1422
1575
  );
1423
1576
  return results.map((row) => row.table_name);
@@ -1473,30 +1626,64 @@ var DrizzleAdapter = class {
1473
1626
  });
1474
1627
  }
1475
1628
  /**
1476
- * Handle query errors and convert to DatabaseError.
1629
+ * Helper to create transaction context CRUD forwarding methods.
1630
+ *
1631
+ * @param txDb - Thunk returning the transaction-bound Drizzle executor
1632
+ * @returns Object providing forwarded CRUD and getDrizzle methods
1633
+ *
1634
+ * @protected
1635
+ */
1636
+ createTransactionForwarders(txDb) {
1637
+ return createTransactionForwarders(this, txDb);
1638
+ }
1639
+ /**
1640
+ * Classify an error into a DatabaseError.
1477
1641
  *
1478
1642
  * @remarks
1479
- * Protected helper for consistent error handling across CRUD operations.
1480
- * Subclasses can override to add dialect-specific error classification.
1643
+ * Subclasses can override this method to add dialect-specific error code classification.
1481
1644
  *
1482
1645
  * @param error - Original error
1483
- * @param operation - Operation that failed
1484
- * @param table - Table name
1646
+ * @param sql - SQL statement that caused the error (optional)
1485
1647
  * @returns DatabaseError instance
1486
1648
  *
1487
1649
  * @protected
1488
1650
  */
1489
- handleQueryError(error, operation, table) {
1651
+ classifyError(error, sql3) {
1490
1652
  if (isDatabaseError(error)) {
1491
1653
  return error;
1492
1654
  }
1493
1655
  const errorMessage = error instanceof Error ? error.message : String(error);
1494
1656
  return this.createDatabaseError(
1495
1657
  "query",
1496
- `${operation} operation failed on table '${table}': ${errorMessage}`,
1658
+ sql3 ? `Query failed: ${errorMessage}` : errorMessage,
1497
1659
  error instanceof Error ? error : void 0
1498
1660
  );
1499
1661
  }
1662
+ /**
1663
+ * Handle query errors and convert to DatabaseError.
1664
+ *
1665
+ * @remarks
1666
+ * Protected helper for consistent error handling across CRUD operations.
1667
+ * Delegates to `classifyError` and attaches operation and table context if not present.
1668
+ * Subclasses override `classifyError` to provide dialect-specific error code classification.
1669
+ *
1670
+ * @param error - Original error
1671
+ * @param operation - Operation that failed
1672
+ * @param table - Table name
1673
+ * @returns DatabaseError instance
1674
+ *
1675
+ * @protected
1676
+ */
1677
+ handleQueryError(error, operation, table) {
1678
+ const dbError = this.classifyError(error);
1679
+ if (!dbError.message.includes(operation)) {
1680
+ dbError.message = `${operation} operation failed on table '${table}': ${dbError.message}`;
1681
+ }
1682
+ if (!dbError.table) {
1683
+ dbError.table = table;
1684
+ }
1685
+ return dbError;
1686
+ }
1500
1687
  };
1501
1688
  var ALLOWED_DEFAULT_SQL_EXPRESSIONS = /* @__PURE__ */ new Set([
1502
1689
  "current_timestamp",