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