@zerotal/orm 1.15.1 → 1.15.2

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/CHANGELOG.md CHANGED
@@ -8,6 +8,25 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.15.2] — 2026-10-23
12
+
13
+ ### Fixed
14
+
15
+ - **The ORM runs on Bun 1.4.4.** Every statement it assembled from a runtime string — the
16
+ string form of `DB.raw()`, the query builder, schema and migration DDL, the `SAVEPOINT`
17
+ a nested transaction issues, the validator's `unique` and `exists` rules — reached Bun's
18
+ `sql` function as a template object built with `Object.assign(parts, { raw: parts })`,
19
+ whose `raw` is enumerable. Bun 1.4.4 refuses that ("has an enumerable 'raw' property, so
20
+ it is not a template object"), so on that version and later none of those statements ran:
21
+ the package's own suite failed 288 tests and then hung. Earlier Bun accepted it, which is
22
+ why nothing noticed until the CI job that runs on the latest Bun did.
23
+
24
+ All of them now take the object from one helper, `_templateStrings()`, which defines
25
+ `raw` the way a real template object has it and freezes the result. `@zerotal/notifications`
26
+ and `@zerotal/testing` had built the same object by hand and use it too. One test runs the
27
+ ORM's paths through a connection that rejects an enumerable `raw`; another fails if any
28
+ package's source builds one again.
29
+
11
30
  ## [1.15.1] — 2026-09-13
12
31
 
13
32
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/orm",
3
- "version": "1.15.1",
3
+ "version": "1.15.2",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -31,8 +31,8 @@
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@zerotal/core": "1.15.1",
35
- "@zerotal/validator": "1.15.1"
34
+ "@zerotal/core": "1.15.2",
35
+ "@zerotal/validator": "1.15.2"
36
36
  },
37
37
  "devDependencies": {
38
38
  "typescript": "^5.8.0"
package/src/db/DB.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  allowNPlusOne as _allowNPlusOne,
18
18
  type NPlusOneOptions,
19
19
  } from "./NPlusOneDetector.ts";
20
+ import { _templateStrings } from "./templateStrings.ts";
20
21
 
21
22
  // Test-override escape hatch — set by _setDbConnection() in test files.
22
23
  // Production code resolves the connection from the container instead.
@@ -140,7 +141,7 @@ let _savepointCounter = 0;
140
141
 
141
142
  /** Run a raw, parameter-less statement on a specific connection (SAVEPOINT, etc.). */
142
143
  function _runRaw(conn: SQLInstance, sql: string): Promise<unknown> {
143
- const arr = Object.assign([sql], { raw: [sql] }) as unknown as TemplateStringsArray;
144
+ const arr = _templateStrings([sql]);
144
145
  return conn(arr);
145
146
  }
146
147
 
@@ -160,7 +161,7 @@ export function _rawStatement(sql: string): Promise<unknown> {
160
161
  /** Run a parameterised statement (`?` placeholders) on a specific connection. */
161
162
  function _runParams(conn: SQLInstance, sql: string, params: unknown[]): Promise<unknown> {
162
163
  const parts = sql.split("?");
163
- const tpl = Object.assign(parts, { raw: parts }) as unknown as TemplateStringsArray;
164
+ const tpl = _templateStrings(parts);
164
165
  return conn(tpl, ...params);
165
166
  }
166
167
 
@@ -246,7 +247,7 @@ export const DB = {
246
247
  if (typeof sql === "string") {
247
248
  const bindings: unknown[] = Array.isArray(rest[0]) ? (rest[0] as unknown[]) : rest;
248
249
  const parts = sql.split("?");
249
- const tpl = Object.assign(parts, { raw: parts }) as unknown as TemplateStringsArray;
250
+ const tpl = _templateStrings(parts);
250
251
  const rows = await conn<T>(tpl, ...bindings);
251
252
  FrameworkEvents.emit(
252
253
  new QueryExecuted(
@@ -20,6 +20,7 @@ import type {
20
20
  SimplePaginateResult,
21
21
  } from "./types.ts";
22
22
  import { withPaginationHelpers, withSimplePaginationHelpers } from "./types.ts";
23
+ import { _templateStrings } from "./templateStrings.ts";
23
24
 
24
25
  /**
25
26
  * The comparison operators accepted in a value-comparison slot. Exported so
@@ -204,8 +205,7 @@ function _getCachedTemplate(strings: string[]): TemplateStringsArray {
204
205
  const key = strings.join("\x00");
205
206
  let tpl = _tplCache.get(key);
206
207
  if (!tpl) {
207
- const arr = [...strings];
208
- tpl = Object.assign(arr, { raw: arr }) as TemplateStringsArray;
208
+ tpl = _templateStrings(strings);
209
209
  if (_tplCache.size >= _TPL_CACHE_MAX) {
210
210
  // FIFO eviction: drop the oldest interned shape.
211
211
  const oldest = _tplCache.keys().next().value;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Build the `strings` argument for a Bun SQL tagged-template call from plain strings.
3
+ *
4
+ * Bun's `sql` function wants what the language hands a tag: a frozen array whose `raw`
5
+ * is a property you cannot see when you enumerate it. `Object.assign(parts, { raw: parts })`
6
+ * makes the property enumerable, and Bun 1.4.4 refuses that with "has an enumerable 'raw'
7
+ * property, so it is not a template object" — every statement built from a runtime-assembled
8
+ * string failed, which is nearly everything the ORM sends. `defineProperty` leaves `raw`
9
+ * non-enumerable, as a real template object has it.
10
+ *
11
+ * Copies its input, so the caller's array is neither frozen nor given a `raw`.
12
+ *
13
+ * @internal Shared by the ORM and by the packages that issue SQL of their own.
14
+ */
15
+ export function _templateStrings(parts: readonly string[]): TemplateStringsArray {
16
+ const strings = [...parts];
17
+ Object.defineProperty(strings, "raw", { value: strings });
18
+ return Object.freeze(strings) as TemplateStringsArray;
19
+ }
package/src/index.ts CHANGED
@@ -97,6 +97,7 @@ export type { InsertPayload, UpdatePayload } from "./model/payload.ts";
97
97
  export { ModelQueryBuilder, _globalScopeRegistry } from "./model/ModelQueryBuilder.ts";
98
98
  export type { GlobalScopeCallback, RelationConstraint } from "./model/ModelQueryBuilder.ts";
99
99
  export { DB } from "./db/DB.ts";
100
+ export { _templateStrings } from "./db/templateStrings.ts";
100
101
  export {
101
102
  _getConnection,
102
103
  _setDbConnection,
@@ -25,6 +25,7 @@ import {
25
25
  RUN_MIGRATIONS_PATH,
26
26
  _mintDiagnosisToken,
27
27
  } from "../diagnostics/runMigrationsEndpoint.ts";
28
+ import { _templateStrings } from "../db/templateStrings.ts";
28
29
 
29
30
  // Extend the core container registry so 'db' is a typed binding.
30
31
  declare module "@zerotal/core" {
@@ -171,27 +172,17 @@ export class DatabaseProvider extends ServiceProvider {
171
172
  let rows: unknown[];
172
173
  if (ignoreId !== undefined) {
173
174
  const strs = [`SELECT 1 FROM ${table} WHERE ${column} = `, ` AND id != `, ` LIMIT 1`];
174
- rows = await conn(
175
- Object.assign(strs, { raw: strs }) as unknown as TemplateStringsArray,
176
- value,
177
- ignoreId,
178
- );
175
+ rows = await conn(_templateStrings(strs), value, ignoreId);
179
176
  } else {
180
177
  const strs = [`SELECT 1 FROM ${table} WHERE ${column} = `, ` LIMIT 1`];
181
- rows = await conn(
182
- Object.assign(strs, { raw: strs }) as unknown as TemplateStringsArray,
183
- value,
184
- );
178
+ rows = await conn(_templateStrings(strs), value);
185
179
  }
186
180
  return rows.length === 0; // true = unique (no duplicate found)
187
181
  }
188
182
 
189
183
  // exists
190
184
  const strs = [`SELECT 1 FROM ${table} WHERE ${column} = `, ` LIMIT 1`];
191
- const rows = await conn(
192
- Object.assign(strs, { raw: strs }) as unknown as TemplateStringsArray,
193
- value,
194
- );
185
+ const rows = await conn(_templateStrings(strs), value);
195
186
  return rows.length > 0; // true = exists (row found)
196
187
  });
197
188
  } catch {
@@ -8,6 +8,7 @@ import { MigrationRan } from "../events.ts";
8
8
  import { dialectFor } from "../db/QueryBuilder.ts";
9
9
  import { getDialect } from "../db/dialects/index.ts";
10
10
  import { TransactionContext } from "../db/TransactionContext.ts";
11
+ import { _templateStrings } from "../db/templateStrings.ts";
11
12
 
12
13
  // ── Public types ──────────────────────────────────────────────────────────────
13
14
 
@@ -375,9 +376,7 @@ export class MigrationRunner {
375
376
  /** Execute a DDL string with no parameters. */
376
377
  private async _ddl(sql: string): Promise<void> {
377
378
  const strings = [sql];
378
- const tpl = Object.assign(strings, {
379
- raw: strings,
380
- }) as TemplateStringsArray;
379
+ const tpl = _templateStrings(strings);
381
380
  await this._active()(tpl);
382
381
  }
383
382
 
@@ -389,22 +388,20 @@ export class MigrationRunner {
389
388
  /** SELECT with exactly one bound parameter. */
390
389
  private async _selectParam<T>(sql: string, value: unknown): Promise<T[]> {
391
390
  const parts = sql.split("?");
392
- const tpl = Object.assign(parts, { raw: parts }) as TemplateStringsArray;
391
+ const tpl = _templateStrings(parts);
393
392
  return this._active()<T>(tpl, value);
394
393
  }
395
394
 
396
395
  /** DML with N bound parameters. */
397
396
  private async _exec(sql: string, ...values: unknown[]): Promise<void> {
398
397
  const parts = sql.split("?");
399
- const tpl = Object.assign(parts, { raw: parts }) as TemplateStringsArray;
398
+ const tpl = _templateStrings(parts);
400
399
  await this._active()(tpl, ...values);
401
400
  }
402
401
 
403
402
  private async _exec0<T>(sql: string): Promise<T[]> {
404
403
  const strings = [sql];
405
- const tpl = Object.assign(strings, {
406
- raw: strings,
407
- }) as TemplateStringsArray;
404
+ const tpl = _templateStrings(strings);
408
405
  return this._active()<T>(tpl);
409
406
  }
410
407
 
@@ -2,6 +2,7 @@ import { _getScopedDbConnection } from "../db/DB.ts";
2
2
  import { _getDialect } from "../model/BaseModel.ts";
3
3
  import { getDialect } from "../db/dialects/index.ts";
4
4
  import { Blueprint } from "./Blueprint.ts";
5
+ import { _templateStrings } from "../db/templateStrings.ts";
5
6
 
6
7
  // ── Helpers ───────────────────────────────────────────────────────────────────
7
8
 
@@ -16,7 +17,7 @@ async function ddl(sql: string): Promise<void> {
16
17
  // migration runner's `begin()` wrapper decorative.
17
18
  const conn = _getScopedDbConnection();
18
19
  const strings = [sql];
19
- const tpl = Object.assign(strings, { raw: strings }) as TemplateStringsArray;
20
+ const tpl = _templateStrings(strings);
20
21
  await conn(tpl);
21
22
  }
22
23
 
@@ -29,7 +30,7 @@ async function query<T = Record<string, unknown>>(sql: string, params: unknown[]
29
30
  // the tables that transaction has created, which a pooled connection cannot.
30
31
  const conn = _getScopedDbConnection();
31
32
  const parts = sql.split("?");
32
- const tpl = Object.assign(parts, { raw: parts }) as TemplateStringsArray;
33
+ const tpl = _templateStrings(parts);
33
34
  return conn<T>(tpl, ...params);
34
35
  }
35
36
 
@@ -1,5 +1,6 @@
1
1
  import { _getDbConnection } from "../db/DB.ts";
2
2
  import { _getDialect } from "../model/BaseModel.ts";
3
+ import { _templateStrings } from "../db/templateStrings.ts";
3
4
 
4
5
  // ── DB column descriptor (normalised across dialects) ─────────────────────────
5
6
 
@@ -23,7 +24,7 @@ export interface LiveTable {
23
24
  async function queryRaw<T>(sql: string): Promise<T[]> {
24
25
  const conn = _getDbConnection();
25
26
  const strings = [sql];
26
- const tpl = Object.assign(strings, { raw: strings }) as TemplateStringsArray;
27
+ const tpl = _templateStrings(strings);
27
28
  return conn<T>(tpl);
28
29
  }
29
30
 
@@ -31,7 +32,7 @@ async function queryRaw<T>(sql: string): Promise<T[]> {
31
32
  async function queryParam<T>(sql: string, value: unknown): Promise<T[]> {
32
33
  const conn = _getDbConnection();
33
34
  const parts = sql.split("?");
34
- const tpl = Object.assign(parts, { raw: parts }) as TemplateStringsArray;
35
+ const tpl = _templateStrings(parts);
35
36
  return conn<T>(tpl, value);
36
37
  }
37
38