@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
@@ -17,8 +17,8 @@
17
17
  * }
18
18
  *
19
19
  * // Code
20
- * const primary = app.container.resolve('db') // default
21
- * const tenant1 = app.container.resolve('db:tenant1') // named
20
+ * const primary = await app.container.resolve('db') // default
21
+ * const tenant1 = await app.container.resolve('db:tenant1') // named
22
22
  */
23
23
 
24
24
  import {
@@ -50,10 +50,30 @@ export interface AtlasAppContext {
50
50
  token: string | symbol | (new (...args: never[]) => unknown),
51
51
  factory: () => unknown,
52
52
  ): void;
53
+ /**
54
+ * Optional reader — present on ream's real container. Used to look up the
55
+ * app emitter (`events`) so query events are bridged onto it as `db:query`
56
+ * (AdonisJS parity: `emitter.on('db:query', …)`). Structural, async-capable.
57
+ */
58
+ resolve?(
59
+ token: string | symbol | (new (...args: never[]) => unknown),
60
+ ): unknown | Promise<unknown>;
53
61
  };
54
62
  config: { get<T = unknown>(key: string): T | undefined };
55
63
  }
56
64
 
65
+ /** True when `x` looks like an event emitter with `emit(event, data)`. */
66
+ function hasEmit(
67
+ x: unknown,
68
+ ): x is { emit: (event: string, data: unknown) => void } {
69
+ return (
70
+ typeof x === "object" &&
71
+ x !== null &&
72
+ "emit" in x &&
73
+ typeof x.emit === "function"
74
+ );
75
+ }
76
+
57
77
  /**
58
78
  * Canonical sqlite production pragma recipe.
59
79
  *
@@ -75,12 +95,32 @@ export const SQLITE_PROD_PRAGMAS = Object.freeze({
75
95
 
76
96
  /** One connection's settings. */
77
97
  export interface ConnectionConfig {
78
- /** Connection URL: "sqlite:data/app.db", "postgres://...", "mysql://..." */
79
- url: string;
80
- /** Minimum pool connections (default: 1) */
98
+ /**
99
+ * Connection URL: "sqlite:data/app.db", "postgres://...", "mysql://...".
100
+ * Lucid names this per-connection key `connection` — {@link connection} is
101
+ * accepted as an alias, so at least one of the two must be set.
102
+ */
103
+ url?: string;
104
+ /**
105
+ * Lucid's per-connection URL key (`connections: { primary: { connection: '…' } }`).
106
+ * Alias of {@link url}; `url` wins when both are present.
107
+ */
108
+ connection?: string;
109
+ /** Minimum pool connections (default: 1). Prefer the Lucid-shaped {@link pool}. */
81
110
  poolMin?: number;
82
- /** Maximum pool connections (default: 10) */
111
+ /** Maximum pool connections (default: 10). Prefer the Lucid-shaped {@link pool}. */
83
112
  poolMax?: number;
113
+ /**
114
+ * Connection-pool sizing, Lucid-shaped — `pool: { min: 2, max: 10 }`. Takes
115
+ * precedence over the flat `poolMin`/`poolMax` when both are set.
116
+ */
117
+ pool?: { min?: number; max?: number };
118
+ /**
119
+ * Emit a `db:query` event for every statement on this connection (Lucid's
120
+ * `debug` connection option). Off by default — subscribe with `onDbQuery`
121
+ * to receive them; with no subscriber nothing is emitted either way.
122
+ */
123
+ debug?: boolean;
84
124
  /**
85
125
  * Connection-level pragmas (sqlite only). Each entry becomes a
86
126
  * `PRAGMA <key> = <value>;` issued before the first query.
@@ -109,18 +149,48 @@ export interface ConnectionConfig {
109
149
 
110
150
  /** Full database config — single-connection (legacy) OR multi-connection. */
111
151
  export interface AtlasDatabaseConfig extends ConnectionConfig {
112
- /** Name of the default connection when `connections` is set. Defaults to `"primary"`. */
152
+ /**
153
+ * Name of the default connection when `connections` is set (Lucid's top-level
154
+ * `connection`). `default` is a deprecated alias — `connection` wins when both
155
+ * are set. Defaults to `"primary"`.
156
+ */
157
+ connection?: string;
158
+ /** @deprecated Use {@link connection} (the Lucid key). */
113
159
  default?: string;
114
160
  /** Named connections. When present, top-level `url` is treated as `connections[default].url`. */
115
161
  connections?: Record<string, ConnectionConfig>;
116
162
  migrations?: {
163
+ /** @deprecated Use the Lucid-shaped {@link paths}. */
117
164
  path?: string;
165
+ /** Migration directories, Lucid-shaped — `paths: ['database/migrations']`. */
166
+ paths?: string[];
118
167
  /**
119
168
  * Custom name for the migrations tracking table. Defaults to `"ream_migrations"`.
120
169
  * Must match `/^[A-Za-z_][A-Za-z0-9_]*$/` — the `MigrationRunner` constructor
121
170
  * throws `AtlasError("MIGRATION_INVALID_TABLE_NAME")` otherwise.
122
171
  */
123
172
  table?: string;
173
+ /**
174
+ * Allow the boot-time auto-migrate in production. OFF by default:
175
+ * starting the app should NOT silently mutate the schema in prod (Adonis
176
+ * Lucid runs migrations only via the explicit `migration:run` command).
177
+ * In non-production, boot auto-migrate stays on for dev convenience. The
178
+ * CLI's `REAM_SKIP_BOOT_MIGRATE=1` always wins.
179
+ */
180
+ autoRunInProduction?: boolean;
181
+ /**
182
+ * Sort migration files with a numeric-aware comparator (`2_x` before
183
+ * `10_x`). Adonis Lucid `migrations.naturalSort`. Defaults to `false`
184
+ * (plain lexicographic, correct for `make:migration`'s fixed-width prefix).
185
+ */
186
+ naturalSort?: boolean;
187
+ /**
188
+ * Run every migration OUTSIDE a transaction. Adonis Lucid
189
+ * `migrations.disableTransactions`. A single migration can also opt out with
190
+ * `static disableTransactions = true`; the effective value is their OR.
191
+ * Defaults to `false`.
192
+ */
193
+ disableTransactions?: boolean;
124
194
  };
125
195
  /**
126
196
  * Boot-time schema verification. When set, atlas reconciles each listed
@@ -140,11 +210,86 @@ export default class AtlasProvider {
140
210
  /** Map of connection name → open connection. Populated at boot. */
141
211
  #connections = new Map<string, AsyncDatabaseConnection>();
142
212
  #defaultName = "primary";
213
+ /** Unsubscribe for the `db:query` → app-emitter bridge, torn down on shutdown. */
214
+ #dbQueryBridge?: () => void;
143
215
 
144
216
  constructor(protected app: AtlasAppContext) {}
145
217
 
218
+ /** Resolve a live connection by name, or throw — never hand out a closed one
219
+ * (used by the `db`/`db:<name>` container factories so a failed-boot or
220
+ * post-shutdown lookup fails loudly instead of returning a dead handle). */
221
+ #requireConnection(name: string): AsyncDatabaseConnection {
222
+ const conn = this.#connections.get(name);
223
+ if (!conn) {
224
+ throw new Error(
225
+ `AtlasProvider: connection '${name}' is not available (boot failed or the provider was shut down).`,
226
+ );
227
+ }
228
+ return conn;
229
+ }
230
+
146
231
  register() {}
147
232
 
233
+ /**
234
+ * Bridge atlas's `db:query` observability onto the app emitter so consumers
235
+ * use the AdonisJS API — `emitter.on('db:query', (query) => …)`. The agnostic
236
+ * `onDbQuery(listener)` primitive stays available for hosts without an emitter.
237
+ * No-op when the container exposes no `events` emitter.
238
+ */
239
+ async #bridgeDbQueryEvents(): Promise<void> {
240
+ const resolve = this.app.container.resolve;
241
+ if (typeof resolve !== "function") return;
242
+ let emitter: unknown;
243
+ try {
244
+ emitter = await resolve.call(this.app.container, "events");
245
+ } catch {
246
+ return; // no emitter bound — `onDbQuery` remains the way to observe
247
+ }
248
+ if (!hasEmit(emitter)) return;
249
+ const { onDbQuery } = await import("./events.js");
250
+ this.#dbQueryBridge?.();
251
+ this.#dbQueryBridge = onDbQuery((event) => {
252
+ emitter.emit("db:query", event);
253
+ });
254
+ }
255
+
256
+ /** Teardown for the connection-lifecycle → app-emitter bridge. */
257
+ #connectionBridge?: () => void;
258
+
259
+ /**
260
+ * Bridge the connection manager's lifecycle events onto the app emitter as
261
+ * AdonisJS's `db:connection:connect` / `db:connection:disconnect` /
262
+ * `db:connection:error` (`[error, node]`). No-op without an `events` emitter.
263
+ */
264
+ async #bridgeConnectionEvents(): Promise<void> {
265
+ const resolve = this.app.container.resolve;
266
+ if (typeof resolve !== "function") return;
267
+ let emitter: unknown;
268
+ try {
269
+ emitter = await resolve.call(this.app.container, "events");
270
+ } catch {
271
+ return;
272
+ }
273
+ if (!hasEmit(emitter)) return;
274
+ const { connectionManager } = await import("./services/db.js");
275
+ const mgr = connectionManager();
276
+ this.#connectionBridge?.();
277
+ const onConnect = (node: unknown) =>
278
+ emitter.emit("db:connection:connect", node);
279
+ const onDisconnect = (node: unknown) =>
280
+ emitter.emit("db:connection:disconnect", node);
281
+ const onError = (node: unknown, err?: unknown) =>
282
+ emitter.emit("db:connection:error", [err, node]);
283
+ mgr.on("connect", onConnect);
284
+ mgr.on("disconnect", onDisconnect);
285
+ mgr.on("error", onError);
286
+ this.#connectionBridge = () => {
287
+ mgr.off("connect", onConnect);
288
+ mgr.off("disconnect", onDisconnect);
289
+ mgr.off("error", onError);
290
+ };
291
+ }
292
+
148
293
  async boot() {
149
294
  const config = this.app.config.get<AtlasDatabaseConfig>("database");
150
295
  if (!config) return;
@@ -153,6 +298,18 @@ export default class AtlasProvider {
153
298
  const { connections, defaultName } = this.#resolveConnections(config);
154
299
  this.#defaultName = defaultName;
155
300
 
301
+ // Bridge query observability onto the app emitter — AdonisJS parity, so
302
+ // consumers write `emitter.on('db:query', …)` (see #bridgeDbQueryEvents).
303
+ await this.#bridgeDbQueryEvents();
304
+ // Bridge connection lifecycle events too, BEFORE opening pools so the boot
305
+ // `connect` events reach the app emitter (`db:connection:connect`).
306
+ await this.#bridgeConnectionEvents();
307
+
308
+ // Import the services/db proxy BEFORE opening any pool: if this import ever
309
+ // failed (packaging/bundle issue), we must not have leaked open connections,
310
+ // and the cleanup catch below needs it available.
311
+ const dbServices = await import("./services/db.js");
312
+
156
313
  // Open every connection in parallel — multi-database apps with slow-to-
157
314
  // handshake drivers (Postgres over TLS, RDS proxies) previously paid the
158
315
  // sum of the round-trip times on boot; now it's the max.
@@ -162,18 +319,24 @@ export default class AtlasProvider {
162
319
  // close every successful one before rethrowing so a partial boot never
163
320
  // leaks pools/sockets.
164
321
  const entries = Object.entries(connections);
322
+ // Register every connection's config up front (Lucid `manager.add`), so the
323
+ // manager knows all connections — including any that fail to open below.
324
+ for (const [name, settings] of entries) {
325
+ dbServices.connectionManager().add(name, settings);
326
+ }
165
327
  const results = await Promise.allSettled(
166
- entries.map(([, settings]) =>
328
+ entries.map(([name, settings]) =>
167
329
  createNapiConnection(
168
330
  settings.url,
169
- settings.poolMin ?? 1,
170
- settings.poolMax ?? 10,
331
+ settings.pool?.min ?? settings.poolMin ?? 1,
332
+ settings.pool?.max ?? settings.poolMax ?? 10,
171
333
  settings.pragmas,
172
334
  {
173
335
  retries: settings.connectRetries,
174
336
  backoffMs: settings.connectBackoffMs,
175
337
  timeoutMs: settings.connectTimeoutMs,
176
338
  },
339
+ { debug: settings.debug ?? false, connectionName: name },
177
340
  ),
178
341
  ),
179
342
  );
@@ -186,6 +349,11 @@ export default class AtlasProvider {
186
349
  else failures.push({ name, error: r.reason });
187
350
  });
188
351
  if (failures.length > 0) {
352
+ // Surface each failure through the manager → app emitter as
353
+ // `db:connection:error` ([error, node]) before rolling back.
354
+ for (const f of failures) {
355
+ dbServices.connectionManager().reportConnectError(f.name, f.error);
356
+ }
189
357
  // Tear down the successes so we don't leak any pool that the runtime
190
358
  // has already opened. Closures run in parallel with allSettled so a
191
359
  // stuck close doesn't block the rollback path.
@@ -201,46 +369,85 @@ export default class AtlasProvider {
201
369
  (others ? ` (also: ${others})` : ""),
202
370
  );
203
371
  }
204
- for (const { name, conn } of successes) {
205
- this.#connections.set(name, conn);
206
- this.app.container.singleton(`db:${name}`, () => conn);
207
- }
372
+ // Everything past this point runs AFTER the pools are open — an invalid
373
+ // default connection, a boot-time migration crash, etc. must not leak the
374
+ // sockets/pools we just opened. On any failure, tear them all down and
375
+ // undo the registry/service bindings before rethrowing.
376
+ try {
377
+ // Validate the default connection opened BEFORE binding anything: a bad
378
+ // default must fail while the container/registry are still untouched, so
379
+ // we never leave handles pointing at connections we're about to close.
380
+ const defaultEntry = successes.find((s) => s.name === defaultName);
381
+ if (!defaultEntry) {
382
+ throw new Error(
383
+ `AtlasProvider: default connection '${defaultName}' is not defined in config.database.connections`,
384
+ );
385
+ }
386
+ const defaultConn = defaultEntry.conn;
208
387
 
209
- // Expose the default under the short aliases `db` and `db.connection`.
210
- const defaultConn = this.#connections.get(defaultName);
211
- if (!defaultConn) {
212
- throw new Error(
213
- `AtlasProvider: default connection '${defaultName}' is not defined in config.database.connections`,
388
+ // Bind connections into the container AND the named-connection registry
389
+ // (the latter is what `BaseModel.connection` / `Factory.connection()` /
390
+ // `getConnection(name)` read). The container factories resolve through
391
+ // `#connections` (not a captured handle) so that if boot fails after
392
+ // binding the catch clears `#connections` or after shutdown, resolving
393
+ // `db`/`db:<name>` throws instead of handing out a CLOSED connection.
394
+ for (const { name, conn } of successes) {
395
+ this.#connections.set(name, conn);
396
+ dbServices.registerConnection(name, conn, connections[name]);
397
+ this.app.container.singleton(`db:${name}`, () =>
398
+ this.#requireConnection(name),
399
+ );
400
+ }
401
+
402
+ // Expose the default under the short aliases `db` and `db.connection`.
403
+ this.app.container.singleton("db", () =>
404
+ this.#requireConnection(this.#defaultName),
405
+ );
406
+ this.app.container.singleton("db.connection", () =>
407
+ this.#requireConnection(this.#defaultName),
214
408
  );
215
- }
216
- this.app.container.singleton("db", () => defaultConn);
217
- this.app.container.singleton("db.connection", () => defaultConn);
218
409
 
219
- // Populate the `@c9up/atlas/services/db` proxy so apps can
220
- // `import db from '@c9up/atlas/services/db'` from anywhere.
221
- // Done inside the lazy-import to avoid pulling the services
222
- // module at construction time when the provider is type-imported
223
- // by `@c9up/ream`'s discovery scan.
224
- const { setDb } = await import("./services/db.js");
225
- setDb(defaultConn);
410
+ // Populate the `@c9up/atlas/services/db` proxy so apps can
411
+ // `import db from '@c9up/atlas/services/db'` from anywhere.
412
+ dbServices.setDb(defaultConn);
226
413
 
227
- // The dialect set module-wide is the DEFAULT connection's dialect.
228
- // Per-connection dialect (when a user hits a non-default) is read from
229
- // the connection URL at query time by each call site that cares.
230
- setAtlasDialect(dialectFromUrl(connections[defaultName]?.url));
414
+ // The dialect set module-wide is the DEFAULT connection's dialect.
415
+ // Per-connection dialect (when a user hits a non-default) is read from
416
+ // the connection URL at query time by each call site that cares.
417
+ setAtlasDialect(dialectFromUrl(connections[defaultName]?.url));
231
418
 
232
- // Auto-run migrations on boot — EXCEPT when a CLI migration command
233
- // (`ream migrate` / `migrate:rollback` / `migrate:status`) booted us: it
234
- // drives migrations explicitly, so a boot-time pass would double-apply
235
- // (and silently re-apply right before a rollback/status). The CLI sets
236
- // `REAM_SKIP_BOOT_MIGRATE=1`; default (unset) preserves boot-migrate.
237
- if (config.migrations?.path && process.env.REAM_SKIP_BOOT_MIGRATE !== "1") {
238
- await this.#runMigrations(
239
- config.migrations.path,
240
- connections[defaultName]?.url,
241
- defaultConn,
242
- config.migrations.table,
243
- );
419
+ // Auto-run migrations on boot — but NOT in production unless explicitly
420
+ // opted in: starting the app should not silently mutate the schema in
421
+ // prod (Adonis Lucid only migrates via `migration:run`). Skipped too
422
+ // when a CLI migration command booted us (`REAM_SKIP_BOOT_MIGRATE=1`),
423
+ // which drives migrations explicitly.
424
+ const inProduction = process.env.NODE_ENV === "production";
425
+ const autoMigrateAllowed =
426
+ !inProduction || config.migrations?.autoRunInProduction === true;
427
+ if (
428
+ (config.migrations?.paths?.[0] ?? config.migrations?.path) &&
429
+ process.env.REAM_SKIP_BOOT_MIGRATE !== "1" &&
430
+ autoMigrateAllowed
431
+ ) {
432
+ await this.#runMigrations(
433
+ config.migrations?.paths?.[0] ?? config.migrations?.path ?? "",
434
+ connections[defaultName]?.url,
435
+ defaultConn,
436
+ config.migrations.table,
437
+ {
438
+ naturalSort: config.migrations.naturalSort,
439
+ disableTransactions: config.migrations.disableTransactions,
440
+ },
441
+ );
442
+ }
443
+ } catch (err) {
444
+ await Promise.allSettled(successes.map((s) => s.conn.close()));
445
+ for (const { name, conn } of successes) {
446
+ dbServices.clearDb(conn);
447
+ dbServices.unregisterConnection(name, conn);
448
+ }
449
+ this.#connections.clear();
450
+ throw err;
244
451
  }
245
452
  }
246
453
 
@@ -254,6 +461,12 @@ export default class AtlasProvider {
254
461
  // - any rejection is aggregated into a single `AggregateError` thrown
255
462
  // at the end so supervisors / health-checks see a non-zero exit
256
463
  // signal instead of a silent "everything is fine" shutdown
464
+ // Detach the db:query → emitter bridge so a re-boot doesn't double-emit.
465
+ this.#dbQueryBridge?.();
466
+ this.#dbQueryBridge = undefined;
467
+ this.#connectionBridge?.();
468
+ this.#connectionBridge = undefined;
469
+
257
470
  const named = [...this.#connections.entries()];
258
471
  const results = await Promise.allSettled(named.map(([, c]) => c.close()));
259
472
  this.#connections.clear();
@@ -261,9 +474,15 @@ export default class AtlasProvider {
261
474
  // Release the module-level singletons this provider populated at boot so a
262
475
  // re-boot starts clean and `db.*` can't dereference a now-closed handle.
263
476
  // `clearDb` is ownership-guarded, so clearing every connection only unbinds
264
- // the one still owning the singleton.
265
- const { clearDb } = await import("./services/db.js");
266
- for (const [, conn] of named) clearDb(conn);
477
+ // the one still owning the singleton. Also drop the named-connection
478
+ // registry entries (boot registered them), so `getConnection(name)` /
479
+ // `BaseModel.connection` / `Factory.connection()` can't hand out a CLOSED
480
+ // handle after shutdown.
481
+ const { clearDb, unregisterConnection } = await import("./services/db.js");
482
+ for (const [name, conn] of named) {
483
+ clearDb(conn);
484
+ unregisterConnection(name, conn);
485
+ }
267
486
  clearCastRegistry();
268
487
  const errors = results
269
488
  .map((r, i) =>
@@ -302,23 +521,51 @@ export default class AtlasProvider {
302
521
 
303
522
  /** Normalize the config into a `{ name → ConnectionConfig }` map + default name. */
304
523
  #resolveConnections(config: AtlasDatabaseConfig): {
305
- connections: Record<string, ConnectionConfig>;
524
+ connections: Record<string, ConnectionConfig & { url: string }>;
306
525
  defaultName: string;
307
526
  } {
308
527
  if (config.connections && Object.keys(config.connections).length > 0) {
528
+ // Normalize each block's URL from Lucid's `connection` key (alias of `url`).
529
+ const connections: Record<string, ConnectionConfig & { url: string }> =
530
+ {};
531
+ for (const [name, block] of Object.entries(config.connections)) {
532
+ const url = block.url ?? block.connection;
533
+ if (!url) {
534
+ throw new Error(
535
+ `[atlas] connection '${name}' has no URL — set 'url' (or Lucid's 'connection') in config.database.connections.${name}.`,
536
+ );
537
+ }
538
+ connections[name] = { ...block, url };
539
+ }
309
540
  return {
310
- connections: config.connections,
311
- defaultName: config.default ?? "primary",
541
+ connections,
542
+ // Lucid's `connection` (default selector); `default` is the alias.
543
+ defaultName: config.connection ?? config.default ?? "primary",
312
544
  };
313
545
  }
314
- // Legacy single-connection shape — promote to multi-connection under "primary".
546
+ // Legacy single-connection shape — promote to multi-connection under
547
+ // "primary". Carry EVERY top-level option through: dropping debug /
548
+ // connectRetries / connectBackoffMs / connectTimeoutMs here silently
549
+ // disabled retry/timeout/query-debug for anyone still on the flat config.
550
+ // (Top-level `connection` is the default-selector, so the flat form's URL
551
+ // comes from `url` only — Lucid's `connection`-as-URL is a nested-block key.)
552
+ if (!config.url) {
553
+ throw new Error(
554
+ "[atlas] no database URL — set 'url' at the top level, or use 'connections: { … }' with a per-connection 'url'/'connection'.",
555
+ );
556
+ }
315
557
  return {
316
558
  connections: {
317
559
  primary: {
318
560
  url: config.url,
319
561
  poolMin: config.poolMin,
320
562
  poolMax: config.poolMax,
563
+ pool: config.pool,
321
564
  pragmas: config.pragmas,
565
+ debug: config.debug,
566
+ connectRetries: config.connectRetries,
567
+ connectBackoffMs: config.connectBackoffMs,
568
+ connectTimeoutMs: config.connectTimeoutMs,
322
569
  },
323
570
  },
324
571
  defaultName: "primary",
@@ -330,6 +577,7 @@ export default class AtlasProvider {
330
577
  url: string,
331
578
  db: AsyncDatabaseConnection,
332
579
  tableName: string | undefined,
580
+ options?: { naturalSort?: boolean; disableTransactions?: boolean },
333
581
  ): Promise<void> {
334
582
  const { existsSync } = await import("node:fs");
335
583
  if (!existsSync(migrationsPath)) return;
@@ -346,11 +594,16 @@ export default class AtlasProvider {
346
594
  // `ream_migrations` bookkeeping row together). Without this, the runner
347
595
  // silently falls back to non-transactional execution.
348
596
  runInTransaction: async (batch) => db.runInTransaction(batch),
597
+ // Managed interactive transaction, so this.defer() migrations run
598
+ // atomically (schema + deferred + tracking committed together).
599
+ transaction: db.transaction?.bind(db),
349
600
  };
350
601
  const runner = new MigrationRunner(adapter, {
351
602
  migrationsDir: migrationsPath,
352
603
  dialect: dialectFromUrl(url),
353
604
  tableName,
605
+ naturalSort: options?.naturalSort,
606
+ disableTransactions: options?.disableTransactions,
354
607
  });
355
608
  await runner.migrate();
356
609
  }