@c9up/atlas 0.1.19 → 0.2.1

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.
Files changed (250) hide show
  1. package/README.md +63 -14
  2. package/db.darwin-arm64.node +0 -0
  3. package/db.darwin-x64.node +0 -0
  4. package/db.linux-arm64-gnu.node +0 -0
  5. package/db.linux-x64-gnu.node +0 -0
  6. package/db.win32-x64-msvc.node +0 -0
  7. package/dist/AtlasProvider.d.ts +66 -7
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +205 -39
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +181 -5
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js +339 -33
  14. package/dist/BaseEntity.js.map +1 -1
  15. package/dist/BaseModel.d.ts +127 -0
  16. package/dist/BaseModel.d.ts.map +1 -0
  17. package/dist/BaseModel.js +250 -0
  18. package/dist/BaseModel.js.map +1 -0
  19. package/dist/BaseRepository.d.ts +81 -15
  20. package/dist/BaseRepository.d.ts.map +1 -1
  21. package/dist/BaseRepository.js +1433 -387
  22. package/dist/BaseRepository.js.map +1 -1
  23. package/dist/ConnectionManager.d.ts +106 -0
  24. package/dist/ConnectionManager.d.ts.map +1 -0
  25. package/dist/ConnectionManager.js +228 -0
  26. package/dist/ConnectionManager.js.map +1 -0
  27. package/dist/ModelQuery.d.ts +577 -41
  28. package/dist/ModelQuery.d.ts.map +1 -1
  29. package/dist/ModelQuery.js +2188 -291
  30. package/dist/ModelQuery.js.map +1 -1
  31. package/dist/Transaction.d.ts +71 -1
  32. package/dist/Transaction.d.ts.map +1 -1
  33. package/dist/Transaction.js +138 -24
  34. package/dist/Transaction.js.map +1 -1
  35. package/dist/adapters/NapiDbAdapter.d.ts +44 -4
  36. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  37. package/dist/adapters/NapiDbAdapter.js +131 -13
  38. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  39. package/dist/console/contract.d.ts +62 -0
  40. package/dist/console/contract.d.ts.map +1 -0
  41. package/dist/console/contract.js +38 -0
  42. package/dist/console/contract.js.map +1 -0
  43. package/dist/console/factoryCommands.d.ts +23 -0
  44. package/dist/console/factoryCommands.d.ts.map +1 -0
  45. package/dist/console/factoryCommands.js +62 -0
  46. package/dist/console/factoryCommands.js.map +1 -0
  47. package/dist/console/migrationCommands.d.ts +78 -0
  48. package/dist/console/migrationCommands.d.ts.map +1 -0
  49. package/dist/console/migrationCommands.js +327 -0
  50. package/dist/console/migrationCommands.js.map +1 -0
  51. package/dist/console/schemaCheckCommand.d.ts +11 -17
  52. package/dist/console/schemaCheckCommand.d.ts.map +1 -1
  53. package/dist/console/schemaCheckCommand.js +21 -15
  54. package/dist/console/schemaCheckCommand.js.map +1 -1
  55. package/dist/console/schemaDumpCommand.d.ts +30 -0
  56. package/dist/console/schemaDumpCommand.d.ts.map +1 -0
  57. package/dist/console/schemaDumpCommand.js +69 -0
  58. package/dist/console/schemaDumpCommand.js.map +1 -0
  59. package/dist/console/schemaGenerateCommand.d.ts +100 -0
  60. package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
  61. package/dist/console/schemaGenerateCommand.js +246 -0
  62. package/dist/console/schemaGenerateCommand.js.map +1 -0
  63. package/dist/console/seederCommands.d.ts +46 -0
  64. package/dist/console/seederCommands.d.ts.map +1 -0
  65. package/dist/console/seederCommands.js +136 -0
  66. package/dist/console/seederCommands.js.map +1 -0
  67. package/dist/decorators/entity.d.ts +50 -8
  68. package/dist/decorators/entity.d.ts.map +1 -1
  69. package/dist/decorators/entity.js +42 -2
  70. package/dist/decorators/entity.js.map +1 -1
  71. package/dist/decorators/hooks.d.ts +3 -3
  72. package/dist/decorators/hooks.d.ts.map +1 -1
  73. package/dist/decorators/hooks.js.map +1 -1
  74. package/dist/events.d.ts +69 -0
  75. package/dist/events.d.ts.map +1 -0
  76. package/dist/events.js +82 -0
  77. package/dist/events.js.map +1 -0
  78. package/dist/factories.d.ts +8 -0
  79. package/dist/factories.d.ts.map +1 -0
  80. package/dist/factories.js +8 -0
  81. package/dist/factories.js.map +1 -0
  82. package/dist/index.d.ts +15 -5
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +14 -3
  85. package/dist/index.js.map +1 -1
  86. package/dist/lucid-schema.d.ts +15 -0
  87. package/dist/lucid-schema.d.ts.map +1 -0
  88. package/dist/lucid-schema.js +16 -0
  89. package/dist/lucid-schema.js.map +1 -0
  90. package/dist/metadata-keys.d.ts +3 -2
  91. package/dist/metadata-keys.d.ts.map +1 -1
  92. package/dist/naming/NamingStrategy.d.ts +7 -0
  93. package/dist/naming/NamingStrategy.d.ts.map +1 -1
  94. package/dist/naming/NamingStrategy.js +16 -0
  95. package/dist/naming/NamingStrategy.js.map +1 -1
  96. package/dist/orm.d.ts +14 -0
  97. package/dist/orm.d.ts.map +1 -0
  98. package/dist/orm.js +14 -0
  99. package/dist/orm.js.map +1 -0
  100. package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
  101. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
  102. package/dist/query/DatabaseQueryBuilder.js +1798 -0
  103. package/dist/query/DatabaseQueryBuilder.js.map +1 -0
  104. package/dist/query/DmlBuilder.d.ts +62 -0
  105. package/dist/query/DmlBuilder.d.ts.map +1 -0
  106. package/dist/query/DmlBuilder.js +84 -0
  107. package/dist/query/DmlBuilder.js.map +1 -0
  108. package/dist/query/QueryBuilder.d.ts +5 -0
  109. package/dist/query/QueryBuilder.d.ts.map +1 -1
  110. package/dist/query/QueryBuilder.js +7 -0
  111. package/dist/query/QueryBuilder.js.map +1 -1
  112. package/dist/query/RawQueryBuilder.d.ts +47 -0
  113. package/dist/query/RawQueryBuilder.d.ts.map +1 -0
  114. package/dist/query/RawQueryBuilder.js +138 -0
  115. package/dist/query/RawQueryBuilder.js.map +1 -0
  116. package/dist/query/interpolate.d.ts +45 -0
  117. package/dist/query/interpolate.d.ts.map +1 -0
  118. package/dist/query/interpolate.js +51 -0
  119. package/dist/query/interpolate.js.map +1 -0
  120. package/dist/query/native.d.ts +8 -0
  121. package/dist/query/native.d.ts.map +1 -1
  122. package/dist/query/native.js +14 -0
  123. package/dist/query/native.js.map +1 -1
  124. package/dist/query/operators.d.ts +4 -0
  125. package/dist/query/operators.d.ts.map +1 -0
  126. package/dist/query/operators.js +27 -0
  127. package/dist/query/operators.js.map +1 -0
  128. package/dist/schema/Migration.d.ts +50 -3
  129. package/dist/schema/Migration.d.ts.map +1 -1
  130. package/dist/schema/Migration.js +60 -24
  131. package/dist/schema/Migration.js.map +1 -1
  132. package/dist/schema/MigrationRunner.d.ts +95 -34
  133. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  134. package/dist/schema/MigrationRunner.js +523 -68
  135. package/dist/schema/MigrationRunner.js.map +1 -1
  136. package/dist/schema/Schema.d.ts +114 -0
  137. package/dist/schema/Schema.d.ts.map +1 -1
  138. package/dist/schema/Schema.js +272 -3
  139. package/dist/schema/Schema.js.map +1 -1
  140. package/dist/schema/SchemaDumper.d.ts +121 -0
  141. package/dist/schema/SchemaDumper.d.ts.map +1 -0
  142. package/dist/schema/SchemaDumper.js +365 -0
  143. package/dist/schema/SchemaDumper.js.map +1 -0
  144. package/dist/schema/Seeder.d.ts +13 -0
  145. package/dist/schema/Seeder.d.ts.map +1 -1
  146. package/dist/schema/Seeder.js +28 -5
  147. package/dist/schema/Seeder.js.map +1 -1
  148. package/dist/schema/TableBuilder.d.ts +274 -16
  149. package/dist/schema/TableBuilder.d.ts.map +1 -1
  150. package/dist/schema/TableBuilder.js +656 -52
  151. package/dist/schema/TableBuilder.js.map +1 -1
  152. package/dist/schema/catalog.d.ts +71 -0
  153. package/dist/schema/catalog.d.ts.map +1 -0
  154. package/dist/schema/catalog.js +158 -0
  155. package/dist/schema/catalog.js.map +1 -0
  156. package/dist/schema/introspect.d.ts +1 -1
  157. package/dist/schema/introspect.d.ts.map +1 -1
  158. package/dist/schema/introspect.js +26 -11
  159. package/dist/schema/introspect.js.map +1 -1
  160. package/dist/schema/types.d.ts +150 -1
  161. package/dist/schema/types.d.ts.map +1 -1
  162. package/dist/schema/types.js +11 -0
  163. package/dist/schema/types.js.map +1 -1
  164. package/dist/seeders.d.ts +7 -0
  165. package/dist/seeders.d.ts.map +1 -0
  166. package/dist/seeders.js +7 -0
  167. package/dist/seeders.js.map +1 -0
  168. package/dist/services/db.d.ts +122 -5
  169. package/dist/services/db.d.ts.map +1 -1
  170. package/dist/services/db.js +187 -14
  171. package/dist/services/db.js.map +1 -1
  172. package/dist/testing/DatabaseCleanup.d.ts +33 -7
  173. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  174. package/dist/testing/DatabaseCleanup.js +54 -24
  175. package/dist/testing/DatabaseCleanup.js.map +1 -1
  176. package/dist/testing/DbAssertions.d.ts +48 -0
  177. package/dist/testing/DbAssertions.d.ts.map +1 -0
  178. package/dist/testing/DbAssertions.js +80 -0
  179. package/dist/testing/DbAssertions.js.map +1 -0
  180. package/dist/testing/Factory.d.ts +166 -15
  181. package/dist/testing/Factory.d.ts.map +1 -1
  182. package/dist/testing/Factory.js +497 -23
  183. package/dist/testing/Factory.js.map +1 -1
  184. package/dist/testing/TestUtils.d.ts +49 -0
  185. package/dist/testing/TestUtils.d.ts.map +1 -0
  186. package/dist/testing/TestUtils.js +79 -0
  187. package/dist/testing/TestUtils.js.map +1 -0
  188. package/dist/testing/index.d.ts +3 -1
  189. package/dist/testing/index.d.ts.map +1 -1
  190. package/dist/testing/index.js +3 -1
  191. package/dist/testing/index.js.map +1 -1
  192. package/dist/types/relations.d.ts +31 -0
  193. package/dist/types/relations.d.ts.map +1 -0
  194. package/dist/types/relations.js +20 -0
  195. package/dist/types/relations.js.map +1 -0
  196. package/index.darwin-arm64.node +0 -0
  197. package/index.darwin-x64.node +0 -0
  198. package/index.linux-arm64-gnu.node +0 -0
  199. package/index.linux-x64-gnu.node +0 -0
  200. package/index.win32-x64-msvc.node +0 -0
  201. package/package.json +32 -2
  202. package/scripts/guard-publish.mjs +15 -0
  203. package/src/AtlasProvider.ts +305 -52
  204. package/src/BaseEntity.ts +460 -41
  205. package/src/BaseModel.ts +461 -0
  206. package/src/BaseRepository.ts +1710 -421
  207. package/src/ConnectionManager.ts +298 -0
  208. package/src/ModelQuery.ts +2998 -335
  209. package/src/Transaction.ts +227 -30
  210. package/src/adapters/NapiDbAdapter.ts +224 -17
  211. package/src/console/contract.ts +96 -0
  212. package/src/console/factoryCommands.ts +73 -0
  213. package/src/console/migrationCommands.ts +448 -0
  214. package/src/console/schemaCheckCommand.ts +26 -22
  215. package/src/console/schemaDumpCommand.ts +96 -0
  216. package/src/console/schemaGenerateCommand.ts +380 -0
  217. package/src/console/seederCommands.ts +174 -0
  218. package/src/decorators/entity.ts +71 -8
  219. package/src/decorators/hooks.ts +5 -3
  220. package/src/events.ts +117 -0
  221. package/src/factories.ts +10 -0
  222. package/src/index.ts +69 -3
  223. package/src/lucid-schema.ts +22 -0
  224. package/src/metadata-keys.ts +3 -2
  225. package/src/naming/NamingStrategy.ts +23 -0
  226. package/src/orm.ts +13 -0
  227. package/src/query/DatabaseQueryBuilder.ts +2724 -0
  228. package/src/query/DmlBuilder.ts +131 -0
  229. package/src/query/QueryBuilder.ts +8 -0
  230. package/src/query/RawQueryBuilder.ts +173 -0
  231. package/src/query/interpolate.ts +73 -0
  232. package/src/query/native.ts +29 -0
  233. package/src/query/operators.ts +27 -0
  234. package/src/schema/Migration.ts +71 -3
  235. package/src/schema/MigrationRunner.ts +676 -69
  236. package/src/schema/Schema.ts +359 -3
  237. package/src/schema/SchemaDumper.ts +518 -0
  238. package/src/schema/Seeder.ts +47 -10
  239. package/src/schema/TableBuilder.ts +783 -52
  240. package/src/schema/catalog.ts +236 -0
  241. package/src/schema/introspect.ts +26 -9
  242. package/src/schema/types.ts +137 -2
  243. package/src/seeders.ts +16 -0
  244. package/src/services/db.ts +361 -21
  245. package/src/testing/DatabaseCleanup.ts +78 -25
  246. package/src/testing/DbAssertions.ts +127 -0
  247. package/src/testing/Factory.ts +837 -40
  248. package/src/testing/TestUtils.ts +123 -0
  249. package/src/testing/index.ts +12 -1
  250. package/src/types/relations.ts +43 -0
@@ -5,55 +5,233 @@
5
5
  */
6
6
 
7
7
  import { randomBytes } from "node:crypto";
8
+ import { EventEmitter } from "node:events";
8
9
  import type { TransactionOptions } from "./adapters/NapiDbAdapter.js";
9
10
  import type { DatabaseConnection } from "./BaseRepository.js";
11
+ import {
12
+ makeTransactionQueryBuilders,
13
+ type TransactionQueryBuilders,
14
+ } from "./query/DatabaseQueryBuilder.js";
15
+ import { getAtlasDialect } from "./query/native.js";
10
16
  import {
11
17
  isTransactionClient,
12
18
  TRANSACTION_BRAND,
13
19
  } from "./utils/transactionBrand.js";
14
20
 
15
- export interface TransactionClient extends DatabaseConnection {
21
+ /** A post-commit / post-rollback side effect (Lucid `trx.after(...)`). */
22
+ export type AfterHook = () => void | Promise<void>;
23
+
24
+ export interface TransactionClient
25
+ extends Omit<DatabaseConnection, "query">,
26
+ TransactionQueryBuilders {
16
27
  commit(): Promise<void>;
17
28
  rollback(): Promise<void>;
29
+ /**
30
+ * Register a side effect to run AFTER the transaction is durable (Lucid
31
+ * `trx.after('commit' | 'rollback', cb)`). A `commit` hook fires only once the
32
+ * ROOT transaction commits — inside a nested (SAVEPOINT) transaction it is
33
+ * forwarded to the parent, so a later outer rollback never runs it. Errors
34
+ * thrown by a hook are swallowed (the caller already saw the transaction
35
+ * succeed).
36
+ */
37
+ after(event: "commit" | "rollback", cb: AfterHook): void;
38
+ /**
39
+ * Subscribe to the client's `commit`/`rollback` EventEmitter (Lucid — the trx
40
+ * client IS a Node EventEmitter). These are SYNCHRONOUS, pre-hook notifications
41
+ * fired the moment this client commits/rolls back — DISTINCT from {@link after},
42
+ * which are durable post-commit hooks that forward to the root when nested.
43
+ */
44
+ on(event: "commit" | "rollback", cb: AfterHook): this;
45
+ /** One-shot {@link on} (Lucid/Node EventEmitter `once`). */
46
+ once(event: "commit" | "rollback", cb: AfterHook): this;
47
+ /** Remove a listener added via {@link on}/{@link once} (Node EventEmitter `off`). */
48
+ off(event: "commit" | "rollback", cb: AfterHook): this;
49
+ /**
50
+ * Open a nested transaction (Lucid `const sp = await trx.transaction()`),
51
+ * implemented as a SAVEPOINT on the same pinned connection. Managed when given
52
+ * a callback (auto RELEASE / ROLLBACK TO), manual otherwise. Works on all three
53
+ * dialects — MySQL's SAVEPOINT statements (which can't be prepared) route
54
+ * through the text protocol in the napi layer.
55
+ */
56
+ transaction(): Promise<TransactionClient>;
57
+ transaction(options: TransactionOptions): Promise<TransactionClient>;
58
+ transaction<T>(
59
+ callback: (trx: TransactionClient) => Promise<T> | T,
60
+ options?: TransactionOptions,
61
+ ): Promise<T>;
18
62
  readonly isNested: boolean;
19
63
  readonly [TRANSACTION_BRAND]: true;
20
64
  }
21
65
 
22
- export async function transaction<T>(
23
- db: DatabaseConnection,
66
+ /**
67
+ * Build the overloaded `transaction()` method for a client. Managed when given a
68
+ * callback (auto RELEASE / ROLLBACK TO), manual otherwise — both open a SAVEPOINT
69
+ * on `getParent()`. The impl signature is deliberately broader than the overloads.
70
+ */
71
+ export function makeNestedTransactionFn(getParent: () => TransactionClient) {
72
+ function tx(): Promise<TransactionClient>;
73
+ function tx(options: TransactionOptions): Promise<TransactionClient>;
74
+ function tx<T>(
75
+ callback: (trx: TransactionClient) => Promise<T> | T,
76
+ options?: TransactionOptions,
77
+ ): Promise<T>;
78
+ function tx(
79
+ arg1?:
80
+ | TransactionOptions
81
+ | ((trx: TransactionClient) => Promise<unknown> | unknown),
82
+ arg2?: TransactionOptions,
83
+ ): Promise<unknown> {
84
+ const parent = getParent();
85
+ const callback = typeof arg1 === "function" ? arg1 : undefined;
86
+ const options = typeof arg1 === "function" ? arg2 : arg1;
87
+ return callback
88
+ ? runManagedSavepoint(parent, callback, options)
89
+ : openSavepoint(parent);
90
+ }
91
+ return tx;
92
+ }
93
+
94
+ /** Managed nested savepoint: open, run the callback, RELEASE on success / ROLLBACK TO on throw. */
95
+ async function runManagedSavepoint<T>(
96
+ parent: TransactionClient,
24
97
  callback: (trx: TransactionClient) => Promise<T> | T,
25
- options?: TransactionOptions,
98
+ _options?: TransactionOptions,
26
99
  ): Promise<T> {
27
- if (isTransactionClient(db)) {
28
- const name = `sp_${randomBytes(6).toString("hex")}`;
29
- await db.execute(`SAVEPOINT ${name}`, []);
30
-
31
- const trx: TransactionClient = {
32
- execute: db.execute.bind(db),
33
- query: db.query.bind(db),
34
- async commit() {
35
- await db.execute(`RELEASE SAVEPOINT ${name}`, []);
36
- },
37
- async rollback() {
38
- await db.execute(`ROLLBACK TO SAVEPOINT ${name}`, []);
39
- },
40
- isNested: true,
41
- [TRANSACTION_BRAND]: true,
42
- };
43
-
100
+ const sp = await openSavepoint(parent);
101
+ try {
102
+ const result = await callback(sp);
103
+ await sp.commit();
104
+ return result;
105
+ } catch (err) {
44
106
  try {
45
- const result = await callback(trx);
46
- await trx.commit();
47
- return result;
48
- } catch (err) {
107
+ await sp.rollback();
108
+ } catch {
109
+ /* best-effort */
110
+ }
111
+ throw err;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Open a SAVEPOINT-backed nested transaction client on `parent`. Shared by the
117
+ * standalone {@link transaction} helper, `trx.transaction()`, and the napi pinned
118
+ * client. Commit RELEASEs the savepoint and forwards hooks to the parent (a
119
+ * nested commit isn't durable until the root commits); rollback does ROLLBACK TO
120
+ * + RELEASE and fires the local rollback hooks.
121
+ */
122
+ export async function openSavepoint(
123
+ parent: TransactionClient,
124
+ ): Promise<TransactionClient> {
125
+ const name = `sp_${randomBytes(6).toString("hex")}`;
126
+ await parent.execute(`SAVEPOINT ${name}`, []);
127
+ const commitHooks: AfterHook[] = [];
128
+ const rollbackHooks: AfterHook[] = [];
129
+ const evt = makeTrxEvents();
130
+ const base = {
131
+ execute: parent.execute.bind(parent),
132
+ query: parent.query.bind(parent),
133
+ async commit(): Promise<void> {
134
+ await parent.execute(`RELEASE SAVEPOINT ${name}`, []);
135
+ evt.emit("commit"); // synchronous EventEmitter notification (this savepoint)
136
+ for (const hook of commitHooks) parent.after("commit", hook);
137
+ for (const hook of rollbackHooks) parent.after("rollback", hook);
138
+ },
139
+ async rollback(): Promise<void> {
140
+ await parent.execute(`ROLLBACK TO SAVEPOINT ${name}`, []);
49
141
  try {
50
- await trx.rollback();
142
+ await parent.execute(`RELEASE SAVEPOINT ${name}`, []);
51
143
  } catch {
52
- /* best-effort */
144
+ /* best-effort — the savepoint is already logically unwound */
53
145
  }
54
- throw err;
146
+ evt.emit("rollback");
147
+ await runAfterHooks(rollbackHooks);
148
+ },
149
+ after(event: "commit" | "rollback", cb: AfterHook): void {
150
+ (event === "commit" ? commitHooks : rollbackHooks).push(cb);
151
+ },
152
+ on: evt.on,
153
+ once: evt.once,
154
+ off: evt.off,
155
+ isNested: true,
156
+ [TRANSACTION_BRAND]: true as const,
157
+ };
158
+ const trx: TransactionClient = Object.assign(
159
+ base,
160
+ makeTransactionQueryBuilders(base, getAtlasDialect()),
161
+ { transaction: makeNestedTransactionFn(() => trx) },
162
+ );
163
+ return trx;
164
+ }
165
+
166
+ /**
167
+ * Run every registered after-hook, swallowing errors — a post-commit side
168
+ * effect must never surface a failure on a transaction the caller already saw
169
+ * commit (Lucid parity). Shared with the napi adapter's pinned-transaction path.
170
+ */
171
+ export async function runAfterHooks(hooks: AfterHook[]): Promise<void> {
172
+ for (const hook of hooks) {
173
+ try {
174
+ await hook();
175
+ } catch {
176
+ /* swallowed — the transaction already succeeded */
55
177
  }
56
178
  }
179
+ }
180
+
181
+ /**
182
+ * The `on`/`once`/`off` EventEmitter surface for a transaction client, plus an
183
+ * internal `emit` the client fires at commit/rollback. Distinct from `after`
184
+ * hooks: these are synchronous, per-client, pre-hook notifications (Lucid — the
185
+ * trx client is a Node EventEmitter). `on`/`once`/`off` return the client for
186
+ * chaining via their `this` binding.
187
+ */
188
+ export function makeTrxEvents(): {
189
+ on(
190
+ this: TransactionClient,
191
+ e: "commit" | "rollback",
192
+ cb: AfterHook,
193
+ ): TransactionClient;
194
+ once(
195
+ this: TransactionClient,
196
+ e: "commit" | "rollback",
197
+ cb: AfterHook,
198
+ ): TransactionClient;
199
+ off(
200
+ this: TransactionClient,
201
+ e: "commit" | "rollback",
202
+ cb: AfterHook,
203
+ ): TransactionClient;
204
+ emit(e: "commit" | "rollback"): void;
205
+ } {
206
+ const ee = new EventEmitter();
207
+ return {
208
+ on(e, cb) {
209
+ ee.on(e, cb);
210
+ return this;
211
+ },
212
+ once(e, cb) {
213
+ ee.once(e, cb);
214
+ return this;
215
+ },
216
+ off(e, cb) {
217
+ ee.off(e, cb);
218
+ return this;
219
+ },
220
+ emit(e) {
221
+ ee.emit(e);
222
+ },
223
+ };
224
+ }
225
+
226
+ export async function transaction<T>(
227
+ db: DatabaseConnection,
228
+ callback: (trx: TransactionClient) => Promise<T> | T,
229
+ options?: TransactionOptions,
230
+ ): Promise<T> {
231
+ // Nested (the caller passed a live trx) → a SAVEPOINT-backed managed savepoint.
232
+ if (isTransactionClient(db)) {
233
+ return runManagedSavepoint(db, callback, options);
234
+ }
57
235
 
58
236
  // Pinned interactive transaction (napi-backed): db.transaction() acquires ONE
59
237
  // connection and issues BEGIN on it; the managed form commits on success /
@@ -68,18 +246,37 @@ export async function transaction<T>(
68
246
 
69
247
  await db.execute("BEGIN", []);
70
248
 
71
- const trx: TransactionClient = {
249
+ const commitHooks: AfterHook[] = [];
250
+ const rollbackHooks: AfterHook[] = [];
251
+ const evt = makeTrxEvents();
252
+
253
+ const base = {
72
254
  execute: db.execute.bind(db),
73
255
  query: db.query.bind(db),
74
256
  async commit() {
75
257
  await db.execute("COMMIT", []);
258
+ evt.emit("commit"); // synchronous EventEmitter notification
259
+ await runAfterHooks(commitHooks);
76
260
  },
77
261
  async rollback() {
78
262
  await db.execute("ROLLBACK", []);
263
+ evt.emit("rollback");
264
+ await runAfterHooks(rollbackHooks);
265
+ },
266
+ after(event: "commit" | "rollback", cb: AfterHook) {
267
+ (event === "commit" ? commitHooks : rollbackHooks).push(cb);
79
268
  },
269
+ on: evt.on,
270
+ once: evt.once,
271
+ off: evt.off,
80
272
  isNested: false,
81
- [TRANSACTION_BRAND]: true,
273
+ [TRANSACTION_BRAND]: true as const,
82
274
  };
275
+ const trx: TransactionClient = Object.assign(
276
+ base,
277
+ makeTransactionQueryBuilders(base, getAtlasDialect()),
278
+ { transaction: makeNestedTransactionFn(() => trx) },
279
+ );
83
280
 
84
281
  try {
85
282
  const result = await callback(trx);
@@ -2,10 +2,54 @@
2
2
  * NapiDbAdapter — bridges the Rust atlas-db NAPI binding to Atlas.
3
3
  */
4
4
 
5
- import type { TransactionClient } from "../Transaction.js";
5
+ import { emitDbQuery, hasDbQueryListeners } from "../events.js";
6
+ import { makeTransactionQueryBuilders } from "../query/DatabaseQueryBuilder.js";
7
+ import {
8
+ type AfterHook,
9
+ makeNestedTransactionFn,
10
+ makeTrxEvents,
11
+ runAfterHooks,
12
+ type TransactionClient,
13
+ } from "../Transaction.js";
6
14
  import { dialectFromUrl } from "../utils/dialectFromUrl.js";
7
15
  import { TRANSACTION_BRAND } from "../utils/transactionBrand.js";
8
16
 
17
+ /**
18
+ * JSON replacer for the napi boundary. `BigInt` throws in a plain
19
+ * `JSON.stringify` and a `Uint8Array`/`Buffer` serializes to a useless
20
+ * index-map, so both are wrapped in envelopes the Rust side decodes and binds
21
+ * losslessly: `{"$bigint": "123"}` → i64, `{"$bytes": "<base64>"}` → BLOB/BYTEA.
22
+ */
23
+ function napiReplacer(_key: string, value: unknown): unknown {
24
+ if (typeof value === "bigint") return { $bigint: value.toString() };
25
+ if (value instanceof Uint8Array) {
26
+ return { $bytes: Buffer.from(value).toString("base64") };
27
+ }
28
+ return value;
29
+ }
30
+
31
+ /**
32
+ * JSON reviver for napi result sets. Rebuilds `{"$bytes": …}` envelopes (emitted
33
+ * by the Rust decoder for BLOB/BYTEA columns) into a `Uint8Array`. Integers
34
+ * beyond JS's safe range arrive pre-stringified by Rust — no precision loss — so
35
+ * they stay strings, matching the pg/mysql driver convention.
36
+ */
37
+ function napiReviver(_key: string, value: unknown): unknown {
38
+ if (
39
+ typeof value === "object" &&
40
+ value !== null &&
41
+ !Array.isArray(value) &&
42
+ "$bytes" in value &&
43
+ Object.keys(value).length === 1
44
+ ) {
45
+ const bytes = value.$bytes;
46
+ if (typeof bytes === "string") {
47
+ return Uint8Array.from(Buffer.from(bytes, "base64"));
48
+ }
49
+ }
50
+ return value;
51
+ }
52
+
9
53
  /** One `(sql, params)` pair passed to `runInTransaction`. */
10
54
  export interface BatchStatement {
11
55
  sql: string;
@@ -28,6 +72,48 @@ export interface TransactionOptions {
28
72
  isolationLevel?: IsolationLevel;
29
73
  }
30
74
 
75
+ /**
76
+ * Context a caller can attach to a statement so the `db:query` event can say
77
+ * where it came from. Optional everywhere — a connection that ignores it stays
78
+ * a valid `AsyncDatabaseConnection`, which is what lets test doubles skip it.
79
+ */
80
+ export interface QueryMeta {
81
+ /** Entity class name, when the statement came from a repository/model. */
82
+ model?: string;
83
+ /** The call that produced it (`exec`, `first`, `paginate`, …). */
84
+ method?: string;
85
+ /** True for schema statements. */
86
+ ddl?: boolean;
87
+ /**
88
+ * Force emission for this statement even when the connection has
89
+ * `debug: false` — this is what `ModelQuery.debug()` sets.
90
+ */
91
+ debug?: boolean;
92
+ /**
93
+ * Arbitrary metadata attached to the `db:query` event (Adonis Lucid
94
+ * `reporterData`) — request id, user id, feature flag, … — for listeners.
95
+ */
96
+ reporterData?: Record<string, unknown>;
97
+ /**
98
+ * Server-side statement timeout in ms (Lucid `timeout(ms, { cancel: true })`).
99
+ * Routes to the driver's `statement_timeout` (Postgres) / `MAX_EXECUTION_TIME`
100
+ * (MySQL SELECT) so the server aborts the query — not just the client race.
101
+ */
102
+ serverTimeoutMs?: number;
103
+ }
104
+
105
+ /** Per-connection observability settings (Lucid's `debug` connection option). */
106
+ export interface ObservabilityOptions {
107
+ /**
108
+ * Emit a `db:query` event for every statement on this connection. Off by
109
+ * default: it costs a timing pair per query, and nothing is emitted anyway
110
+ * unless something subscribed via `onDbQuery`.
111
+ */
112
+ debug?: boolean;
113
+ /** Connection name, reported on each event so multi-connection apps can tell them apart. */
114
+ connectionName?: string;
115
+ }
116
+
31
117
  /** Async database connection backed by Rust (sqlx). */
32
118
  export interface AsyncDatabaseConnection {
33
119
  /** The dialect this connection targets — derived from the URL scheme at connect time. */
@@ -35,8 +121,13 @@ export interface AsyncDatabaseConnection {
35
121
  query<T = Record<string, unknown>>(
36
122
  sql: string,
37
123
  params?: unknown[],
124
+ meta?: QueryMeta,
38
125
  ): Promise<T[]>;
39
- execute(sql: string, params?: unknown[]): Promise<{ rowsAffected: number }>;
126
+ execute(
127
+ sql: string,
128
+ params?: unknown[],
129
+ meta?: QueryMeta,
130
+ ): Promise<{ rowsAffected: number }>;
40
131
  /**
41
132
  * Run every statement in `batch` atomically inside a single sqlx transaction.
42
133
  * Either every statement commits or none do — used by MigrationRunner to
@@ -72,7 +163,7 @@ export interface AsyncDatabaseConnection {
72
163
  /** Shape of the NAPI ReamTransaction handle returned by `ReamDatabase.begin()`. */
73
164
  interface NapiReamTransaction {
74
165
  query(sql: string, paramsJson: string): Promise<string>;
75
- execute(sql: string, paramsJson: string): Promise<number>;
166
+ execute(sql: string, paramsJson: string): Promise<string>;
76
167
  commit(): Promise<void>;
77
168
  rollback(): Promise<void>;
78
169
  }
@@ -80,7 +171,19 @@ interface NapiReamTransaction {
80
171
  /** Shape of the NAPI ReamDatabase class. */
81
172
  interface NapiReamDatabase {
82
173
  query(sql: string, paramsJson: string): Promise<string>;
83
- execute(sql: string, paramsJson: string): Promise<number>;
174
+ execute(sql: string, paramsJson: string): Promise<string>;
175
+ /** query with a server-side statement timeout (Lucid `timeout(ms,{cancel:true})`). */
176
+ queryTimed(
177
+ sql: string,
178
+ paramsJson: string,
179
+ timeoutMs: number,
180
+ ): Promise<string>;
181
+ /** execute with a server-side statement timeout (Postgres). */
182
+ executeTimed(
183
+ sql: string,
184
+ paramsJson: string,
185
+ timeoutMs: number,
186
+ ): Promise<string>;
84
187
  runInTransaction(batchJson: string): Promise<number>;
85
188
  begin(isolationLevel?: string): Promise<NapiReamTransaction>;
86
189
  close(): Promise<void>;
@@ -129,7 +232,9 @@ export async function createNapiConnection(
129
232
  poolMax = 10,
130
233
  pragmas?: Record<string, string | number>,
131
234
  retry?: ConnectRetryOptions,
235
+ observability: ObservabilityOptions = {},
132
236
  ): Promise<AsyncDatabaseConnection> {
237
+ const { debug = false, connectionName } = observability;
133
238
  // Throws with the underlying cause if the binary can't be loaded.
134
239
  const native = await loadNativeDb();
135
240
 
@@ -184,30 +289,65 @@ export async function createNapiConnection(
184
289
  isolationLevel?: IsolationLevel,
185
290
  ): Promise<TransactionClient> {
186
291
  const native = await db.begin(isolationLevel);
187
- return {
292
+ // Root (non-nested) transaction: after-hooks fire once the underlying
293
+ // COMMIT / ROLLBACK is durable (Lucid `trx.after(...)`), errors swallowed.
294
+ const commitHooks: AfterHook[] = [];
295
+ const rollbackHooks: AfterHook[] = [];
296
+ const evt = makeTrxEvents();
297
+ const base = {
188
298
  async execute(
189
299
  sql: string,
190
300
  params: unknown[] = [],
191
- ): Promise<{ rowsAffected: number }> {
192
- const affected = await native.execute(sql, JSON.stringify(params));
193
- return { rowsAffected: affected };
301
+ ): Promise<{ rowsAffected: number; lastInsertId?: number }> {
302
+ const json = await native.execute(
303
+ sql,
304
+ JSON.stringify(params, napiReplacer),
305
+ );
306
+ const r = JSON.parse(json);
307
+ return {
308
+ rowsAffected: Number(r.rows_affected),
309
+ lastInsertId:
310
+ r.last_insert_id == null ? undefined : Number(r.last_insert_id),
311
+ };
194
312
  },
195
313
  async query<T = Record<string, unknown>>(
196
314
  sql: string,
197
315
  params: unknown[] = [],
198
316
  ): Promise<T[]> {
199
- const json = await native.query(sql, JSON.stringify(params));
200
- return JSON.parse(json) as T[];
317
+ const json = await native.query(
318
+ sql,
319
+ JSON.stringify(params, napiReplacer),
320
+ );
321
+ return JSON.parse(json, napiReviver) as T[];
201
322
  },
202
323
  async commit(): Promise<void> {
203
324
  await native.commit();
325
+ evt.emit("commit"); // synchronous EventEmitter notification (Lucid trx.on)
326
+ await runAfterHooks(commitHooks);
204
327
  },
205
328
  async rollback(): Promise<void> {
206
329
  await native.rollback();
330
+ evt.emit("rollback");
331
+ await runAfterHooks(rollbackHooks);
207
332
  },
333
+ after(event: "commit" | "rollback", cb: AfterHook): void {
334
+ (event === "commit" ? commitHooks : rollbackHooks).push(cb);
335
+ },
336
+ on: evt.on,
337
+ once: evt.once,
338
+ off: evt.off,
208
339
  isNested: false,
209
- [TRANSACTION_BRAND]: true,
340
+ [TRANSACTION_BRAND]: true as const,
210
341
  };
342
+ // Lucid: the transaction client is also a query-builder entry point
343
+ // (trx.table()/from()/insertQuery()), routed through THIS pinned connection.
344
+ // `trx.transaction()` opens a SAVEPOINT-backed nested client on this one.
345
+ const client: TransactionClient = Object.assign(
346
+ base,
347
+ makeTransactionQueryBuilders(base, dialect),
348
+ { transaction: makeNestedTransactionFn(() => client) },
349
+ );
350
+ return client;
211
351
  }
212
352
 
213
353
  // Lucid-compatible `transaction`: managed when given a callback (auto
@@ -240,29 +380,96 @@ export async function createNapiConnection(
240
380
  }
241
381
  }
242
382
 
383
+ /**
384
+ * Run `fn`, emitting a `db:query` event around it when observation is on.
385
+ *
386
+ * The fast path is a single boolean pair: with no listeners, or with debug
387
+ * off and no per-query override, this adds nothing but the check. The event
388
+ * is emitted on failure too — a slow query that then throws is exactly the
389
+ * one worth seeing.
390
+ */
391
+ async function observed<T>(
392
+ sql: string,
393
+ params: unknown[],
394
+ meta: QueryMeta | undefined,
395
+ fn: () => Promise<T>,
396
+ ): Promise<T> {
397
+ if (!(debug || meta?.debug) || !hasDbQueryListeners()) return fn();
398
+
399
+ const startedAt = performance.now();
400
+ try {
401
+ const result = await fn();
402
+ emitDbQuery({
403
+ sql,
404
+ bindings: params,
405
+ duration: performance.now() - startedAt,
406
+ connection: connectionName,
407
+ model: meta?.model,
408
+ method: meta?.method,
409
+ ddl: meta?.ddl,
410
+ inTransaction: false,
411
+ reporterData: meta?.reporterData,
412
+ });
413
+ return result;
414
+ } catch (error) {
415
+ emitDbQuery({
416
+ sql,
417
+ bindings: params,
418
+ duration: performance.now() - startedAt,
419
+ connection: connectionName,
420
+ model: meta?.model,
421
+ method: meta?.method,
422
+ ddl: meta?.ddl,
423
+ inTransaction: false,
424
+ error: error instanceof Error ? error : new Error(String(error)),
425
+ reporterData: meta?.reporterData,
426
+ });
427
+ throw error;
428
+ }
429
+ }
430
+
243
431
  return {
244
432
  dialect,
245
433
  transaction,
246
434
  async query<T = Record<string, unknown>>(
247
435
  sql: string,
248
436
  params: unknown[] = [],
437
+ meta?: QueryMeta,
249
438
  ): Promise<T[]> {
250
- const json = await db.query(sql, JSON.stringify(params));
251
- return JSON.parse(json) as T[];
439
+ return observed(sql, params, meta, async () => {
440
+ const paramsJson = JSON.stringify(params, napiReplacer);
441
+ const json =
442
+ meta?.serverTimeoutMs != null
443
+ ? await db.queryTimed(sql, paramsJson, meta.serverTimeoutMs)
444
+ : await db.query(sql, paramsJson);
445
+ return JSON.parse(json, napiReviver) as T[];
446
+ });
252
447
  },
253
448
 
254
449
  async execute(
255
450
  sql: string,
256
451
  params: unknown[] = [],
257
- ): Promise<{ rowsAffected: number }> {
258
- const affected = await db.execute(sql, JSON.stringify(params));
259
- return { rowsAffected: affected };
452
+ meta?: QueryMeta,
453
+ ): Promise<{ rowsAffected: number; lastInsertId?: number }> {
454
+ return observed(sql, params, meta, async () => {
455
+ const paramsJson = JSON.stringify(params, napiReplacer);
456
+ const json =
457
+ meta?.serverTimeoutMs != null
458
+ ? await db.executeTimed(sql, paramsJson, meta.serverTimeoutMs)
459
+ : await db.execute(sql, paramsJson);
460
+ const r = JSON.parse(json);
461
+ return {
462
+ rowsAffected: Number(r.rows_affected),
463
+ lastInsertId:
464
+ r.last_insert_id == null ? undefined : Number(r.last_insert_id),
465
+ };
466
+ });
260
467
  },
261
468
 
262
469
  async runInTransaction(batch: readonly BatchStatement[]): Promise<number> {
263
470
  // Rust side expects `[[sql, params], ...]`
264
471
  const payload = batch.map((s) => [s.sql, s.params ?? []]);
265
- return db.runInTransaction(JSON.stringify(payload));
472
+ return db.runInTransaction(JSON.stringify(payload, napiReplacer));
266
473
  },
267
474
 
268
475
  async close(): Promise<void> {