@c9up/atlas 0.2.0 → 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 (234) hide show
  1. package/README.md +8 -0
  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 +60 -7
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +204 -38
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +17 -0
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js.map +1 -1
  14. package/dist/BaseModel.d.ts +63 -27
  15. package/dist/BaseModel.d.ts.map +1 -1
  16. package/dist/BaseModel.js +108 -51
  17. package/dist/BaseModel.js.map +1 -1
  18. package/dist/BaseRepository.d.ts +11 -1
  19. package/dist/BaseRepository.d.ts.map +1 -1
  20. package/dist/BaseRepository.js +95 -20
  21. package/dist/BaseRepository.js.map +1 -1
  22. package/dist/ConnectionManager.d.ts +106 -0
  23. package/dist/ConnectionManager.d.ts.map +1 -0
  24. package/dist/ConnectionManager.js +228 -0
  25. package/dist/ConnectionManager.js.map +1 -0
  26. package/dist/ModelQuery.d.ts +256 -43
  27. package/dist/ModelQuery.d.ts.map +1 -1
  28. package/dist/ModelQuery.js +899 -231
  29. package/dist/ModelQuery.js.map +1 -1
  30. package/dist/Transaction.d.ts +54 -1
  31. package/dist/Transaction.d.ts.map +1 -1
  32. package/dist/Transaction.js +121 -59
  33. package/dist/Transaction.js.map +1 -1
  34. package/dist/adapters/NapiDbAdapter.d.ts +11 -0
  35. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  36. package/dist/adapters/NapiDbAdapter.js +35 -7
  37. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  38. package/dist/console/contract.d.ts +62 -0
  39. package/dist/console/contract.d.ts.map +1 -0
  40. package/dist/console/contract.js +38 -0
  41. package/dist/console/contract.js.map +1 -0
  42. package/dist/console/factoryCommands.d.ts +23 -0
  43. package/dist/console/factoryCommands.d.ts.map +1 -0
  44. package/dist/console/factoryCommands.js +62 -0
  45. package/dist/console/factoryCommands.js.map +1 -0
  46. package/dist/console/migrationCommands.d.ts +38 -8
  47. package/dist/console/migrationCommands.d.ts.map +1 -1
  48. package/dist/console/migrationCommands.js +157 -50
  49. package/dist/console/migrationCommands.js.map +1 -1
  50. package/dist/console/schemaCheckCommand.d.ts +11 -17
  51. package/dist/console/schemaCheckCommand.d.ts.map +1 -1
  52. package/dist/console/schemaCheckCommand.js +21 -15
  53. package/dist/console/schemaCheckCommand.js.map +1 -1
  54. package/dist/console/schemaDumpCommand.d.ts +30 -0
  55. package/dist/console/schemaDumpCommand.d.ts.map +1 -0
  56. package/dist/console/schemaDumpCommand.js +69 -0
  57. package/dist/console/schemaDumpCommand.js.map +1 -0
  58. package/dist/console/schemaGenerateCommand.d.ts +100 -0
  59. package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
  60. package/dist/console/schemaGenerateCommand.js +246 -0
  61. package/dist/console/schemaGenerateCommand.js.map +1 -0
  62. package/dist/console/seederCommands.d.ts +46 -0
  63. package/dist/console/seederCommands.d.ts.map +1 -0
  64. package/dist/console/seederCommands.js +136 -0
  65. package/dist/console/seederCommands.js.map +1 -0
  66. package/dist/decorators/entity.d.ts +13 -2
  67. package/dist/decorators/entity.d.ts.map +1 -1
  68. package/dist/decorators/entity.js +10 -0
  69. package/dist/decorators/entity.js.map +1 -1
  70. package/dist/decorators/hooks.d.ts +3 -3
  71. package/dist/decorators/hooks.d.ts.map +1 -1
  72. package/dist/decorators/hooks.js.map +1 -1
  73. package/dist/events.d.ts +5 -0
  74. package/dist/events.d.ts.map +1 -1
  75. package/dist/events.js.map +1 -1
  76. package/dist/factories.d.ts +8 -0
  77. package/dist/factories.d.ts.map +1 -0
  78. package/dist/factories.js +8 -0
  79. package/dist/factories.js.map +1 -0
  80. package/dist/index.d.ts +11 -5
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +12 -4
  83. package/dist/index.js.map +1 -1
  84. package/dist/lucid-schema.d.ts +15 -0
  85. package/dist/lucid-schema.d.ts.map +1 -0
  86. package/dist/lucid-schema.js +16 -0
  87. package/dist/lucid-schema.js.map +1 -0
  88. package/dist/orm.d.ts +14 -0
  89. package/dist/orm.d.ts.map +1 -0
  90. package/dist/orm.js +14 -0
  91. package/dist/orm.js.map +1 -0
  92. package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
  93. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
  94. package/dist/query/DatabaseQueryBuilder.js +1798 -0
  95. package/dist/query/DatabaseQueryBuilder.js.map +1 -0
  96. package/dist/query/DmlBuilder.d.ts +62 -0
  97. package/dist/query/DmlBuilder.d.ts.map +1 -0
  98. package/dist/query/DmlBuilder.js +84 -0
  99. package/dist/query/DmlBuilder.js.map +1 -0
  100. package/dist/query/QueryBuilder.d.ts +5 -0
  101. package/dist/query/QueryBuilder.d.ts.map +1 -1
  102. package/dist/query/QueryBuilder.js +7 -0
  103. package/dist/query/QueryBuilder.js.map +1 -1
  104. package/dist/query/RawQueryBuilder.d.ts +47 -0
  105. package/dist/query/RawQueryBuilder.d.ts.map +1 -0
  106. package/dist/query/RawQueryBuilder.js +138 -0
  107. package/dist/query/RawQueryBuilder.js.map +1 -0
  108. package/dist/query/interpolate.d.ts +45 -0
  109. package/dist/query/interpolate.d.ts.map +1 -0
  110. package/dist/query/interpolate.js +51 -0
  111. package/dist/query/interpolate.js.map +1 -0
  112. package/dist/query/native.d.ts +8 -0
  113. package/dist/query/native.d.ts.map +1 -1
  114. package/dist/query/native.js +14 -0
  115. package/dist/query/native.js.map +1 -1
  116. package/dist/query/operators.d.ts +4 -0
  117. package/dist/query/operators.d.ts.map +1 -0
  118. package/dist/query/operators.js +27 -0
  119. package/dist/query/operators.js.map +1 -0
  120. package/dist/schema/Migration.d.ts +31 -7
  121. package/dist/schema/Migration.d.ts.map +1 -1
  122. package/dist/schema/Migration.js +34 -7
  123. package/dist/schema/Migration.js.map +1 -1
  124. package/dist/schema/MigrationRunner.d.ts +56 -6
  125. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  126. package/dist/schema/MigrationRunner.js +382 -112
  127. package/dist/schema/MigrationRunner.js.map +1 -1
  128. package/dist/schema/Schema.d.ts +59 -2
  129. package/dist/schema/Schema.d.ts.map +1 -1
  130. package/dist/schema/Schema.js +143 -9
  131. package/dist/schema/Schema.js.map +1 -1
  132. package/dist/schema/SchemaDumper.d.ts +121 -0
  133. package/dist/schema/SchemaDumper.d.ts.map +1 -0
  134. package/dist/schema/SchemaDumper.js +365 -0
  135. package/dist/schema/SchemaDumper.js.map +1 -0
  136. package/dist/schema/Seeder.d.ts +13 -0
  137. package/dist/schema/Seeder.d.ts.map +1 -1
  138. package/dist/schema/Seeder.js +28 -5
  139. package/dist/schema/Seeder.js.map +1 -1
  140. package/dist/schema/TableBuilder.d.ts +27 -8
  141. package/dist/schema/TableBuilder.d.ts.map +1 -1
  142. package/dist/schema/TableBuilder.js +50 -12
  143. package/dist/schema/TableBuilder.js.map +1 -1
  144. package/dist/schema/catalog.d.ts +25 -1
  145. package/dist/schema/catalog.d.ts.map +1 -1
  146. package/dist/schema/catalog.js +69 -22
  147. package/dist/schema/catalog.js.map +1 -1
  148. package/dist/schema/introspect.d.ts +1 -1
  149. package/dist/schema/introspect.d.ts.map +1 -1
  150. package/dist/schema/introspect.js +26 -11
  151. package/dist/schema/introspect.js.map +1 -1
  152. package/dist/seeders.d.ts +7 -0
  153. package/dist/seeders.d.ts.map +1 -0
  154. package/dist/seeders.js +7 -0
  155. package/dist/seeders.js.map +1 -0
  156. package/dist/services/db.d.ts +120 -9
  157. package/dist/services/db.d.ts.map +1 -1
  158. package/dist/services/db.js +182 -26
  159. package/dist/services/db.js.map +1 -1
  160. package/dist/testing/DatabaseCleanup.d.ts +29 -6
  161. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  162. package/dist/testing/DatabaseCleanup.js +44 -17
  163. package/dist/testing/DatabaseCleanup.js.map +1 -1
  164. package/dist/testing/DbAssertions.d.ts +48 -0
  165. package/dist/testing/DbAssertions.d.ts.map +1 -0
  166. package/dist/testing/DbAssertions.js +80 -0
  167. package/dist/testing/DbAssertions.js.map +1 -0
  168. package/dist/testing/Factory.d.ts +111 -25
  169. package/dist/testing/Factory.d.ts.map +1 -1
  170. package/dist/testing/Factory.js +331 -56
  171. package/dist/testing/Factory.js.map +1 -1
  172. package/dist/testing/TestUtils.d.ts +49 -0
  173. package/dist/testing/TestUtils.d.ts.map +1 -0
  174. package/dist/testing/TestUtils.js +79 -0
  175. package/dist/testing/TestUtils.js.map +1 -0
  176. package/dist/testing/index.d.ts +3 -1
  177. package/dist/testing/index.d.ts.map +1 -1
  178. package/dist/testing/index.js +3 -1
  179. package/dist/testing/index.js.map +1 -1
  180. package/dist/types/relations.d.ts +31 -0
  181. package/dist/types/relations.d.ts.map +1 -0
  182. package/dist/types/relations.js +20 -0
  183. package/dist/types/relations.js.map +1 -0
  184. package/index.darwin-arm64.node +0 -0
  185. package/index.darwin-x64.node +0 -0
  186. package/index.linux-arm64-gnu.node +0 -0
  187. package/index.linux-x64-gnu.node +0 -0
  188. package/index.win32-x64-msvc.node +0 -0
  189. package/package.json +30 -2
  190. package/src/AtlasProvider.ts +297 -51
  191. package/src/BaseEntity.ts +29 -0
  192. package/src/BaseModel.ts +174 -37
  193. package/src/BaseRepository.ts +136 -17
  194. package/src/ConnectionManager.ts +298 -0
  195. package/src/ModelQuery.ts +1309 -260
  196. package/src/Transaction.ts +194 -60
  197. package/src/adapters/NapiDbAdapter.ts +72 -14
  198. package/src/console/contract.ts +96 -0
  199. package/src/console/factoryCommands.ts +73 -0
  200. package/src/console/migrationCommands.ts +253 -63
  201. package/src/console/schemaCheckCommand.ts +26 -22
  202. package/src/console/schemaDumpCommand.ts +96 -0
  203. package/src/console/schemaGenerateCommand.ts +380 -0
  204. package/src/console/seederCommands.ts +174 -0
  205. package/src/decorators/entity.ts +18 -2
  206. package/src/decorators/hooks.ts +5 -3
  207. package/src/events.ts +5 -0
  208. package/src/factories.ts +10 -0
  209. package/src/index.ts +50 -3
  210. package/src/lucid-schema.ts +22 -0
  211. package/src/orm.ts +13 -0
  212. package/src/query/DatabaseQueryBuilder.ts +2724 -0
  213. package/src/query/DmlBuilder.ts +131 -0
  214. package/src/query/QueryBuilder.ts +8 -0
  215. package/src/query/RawQueryBuilder.ts +173 -0
  216. package/src/query/interpolate.ts +73 -0
  217. package/src/query/native.ts +29 -0
  218. package/src/query/operators.ts +27 -0
  219. package/src/schema/Migration.ts +36 -7
  220. package/src/schema/MigrationRunner.ts +483 -119
  221. package/src/schema/Schema.ts +168 -19
  222. package/src/schema/SchemaDumper.ts +518 -0
  223. package/src/schema/Seeder.ts +47 -10
  224. package/src/schema/TableBuilder.ts +48 -11
  225. package/src/schema/catalog.ts +94 -24
  226. package/src/schema/introspect.ts +26 -9
  227. package/src/seeders.ts +16 -0
  228. package/src/services/db.ts +343 -31
  229. package/src/testing/DatabaseCleanup.ts +73 -21
  230. package/src/testing/DbAssertions.ts +127 -0
  231. package/src/testing/Factory.ts +559 -79
  232. package/src/testing/TestUtils.ts +123 -0
  233. package/src/testing/index.ts +12 -1
  234. 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,26 @@ 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 };
84
118
  /**
85
119
  * Emit a `db:query` event for every statement on this connection (Lucid's
86
120
  * `debug` connection option). Off by default — subscribe with `onDbQuery`
@@ -115,18 +149,48 @@ export interface ConnectionConfig {
115
149
 
116
150
  /** Full database config — single-connection (legacy) OR multi-connection. */
117
151
  export interface AtlasDatabaseConfig extends ConnectionConfig {
118
- /** 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). */
119
159
  default?: string;
120
160
  /** Named connections. When present, top-level `url` is treated as `connections[default].url`. */
121
161
  connections?: Record<string, ConnectionConfig>;
122
162
  migrations?: {
163
+ /** @deprecated Use the Lucid-shaped {@link paths}. */
123
164
  path?: string;
165
+ /** Migration directories, Lucid-shaped — `paths: ['database/migrations']`. */
166
+ paths?: string[];
124
167
  /**
125
168
  * Custom name for the migrations tracking table. Defaults to `"ream_migrations"`.
126
169
  * Must match `/^[A-Za-z_][A-Za-z0-9_]*$/` — the `MigrationRunner` constructor
127
170
  * throws `AtlasError("MIGRATION_INVALID_TABLE_NAME")` otherwise.
128
171
  */
129
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;
130
194
  };
131
195
  /**
132
196
  * Boot-time schema verification. When set, atlas reconciles each listed
@@ -146,11 +210,86 @@ export default class AtlasProvider {
146
210
  /** Map of connection name → open connection. Populated at boot. */
147
211
  #connections = new Map<string, AsyncDatabaseConnection>();
148
212
  #defaultName = "primary";
213
+ /** Unsubscribe for the `db:query` → app-emitter bridge, torn down on shutdown. */
214
+ #dbQueryBridge?: () => void;
149
215
 
150
216
  constructor(protected app: AtlasAppContext) {}
151
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
+
152
231
  register() {}
153
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
+
154
293
  async boot() {
155
294
  const config = this.app.config.get<AtlasDatabaseConfig>("database");
156
295
  if (!config) return;
@@ -159,6 +298,18 @@ export default class AtlasProvider {
159
298
  const { connections, defaultName } = this.#resolveConnections(config);
160
299
  this.#defaultName = defaultName;
161
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
+
162
313
  // Open every connection in parallel — multi-database apps with slow-to-
163
314
  // handshake drivers (Postgres over TLS, RDS proxies) previously paid the
164
315
  // sum of the round-trip times on boot; now it's the max.
@@ -168,12 +319,17 @@ export default class AtlasProvider {
168
319
  // close every successful one before rethrowing so a partial boot never
169
320
  // leaks pools/sockets.
170
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
+ }
171
327
  const results = await Promise.allSettled(
172
328
  entries.map(([name, settings]) =>
173
329
  createNapiConnection(
174
330
  settings.url,
175
- settings.poolMin ?? 1,
176
- settings.poolMax ?? 10,
331
+ settings.pool?.min ?? settings.poolMin ?? 1,
332
+ settings.pool?.max ?? settings.poolMax ?? 10,
177
333
  settings.pragmas,
178
334
  {
179
335
  retries: settings.connectRetries,
@@ -193,6 +349,11 @@ export default class AtlasProvider {
193
349
  else failures.push({ name, error: r.reason });
194
350
  });
195
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
+ }
196
357
  // Tear down the successes so we don't leak any pool that the runtime
197
358
  // has already opened. Closures run in parallel with allSettled so a
198
359
  // stuck close doesn't block the rollback path.
@@ -208,46 +369,85 @@ export default class AtlasProvider {
208
369
  (others ? ` (also: ${others})` : ""),
209
370
  );
210
371
  }
211
- for (const { name, conn } of successes) {
212
- this.#connections.set(name, conn);
213
- this.app.container.singleton(`db:${name}`, () => conn);
214
- }
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;
215
387
 
216
- // Expose the default under the short aliases `db` and `db.connection`.
217
- const defaultConn = this.#connections.get(defaultName);
218
- if (!defaultConn) {
219
- throw new Error(
220
- `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),
221
408
  );
222
- }
223
- this.app.container.singleton("db", () => defaultConn);
224
- this.app.container.singleton("db.connection", () => defaultConn);
225
409
 
226
- // Populate the `@c9up/atlas/services/db` proxy so apps can
227
- // `import db from '@c9up/atlas/services/db'` from anywhere.
228
- // Done inside the lazy-import to avoid pulling the services
229
- // module at construction time when the provider is type-imported
230
- // by `@c9up/ream`'s discovery scan.
231
- const { setDb } = await import("./services/db.js");
232
- 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);
233
413
 
234
- // The dialect set module-wide is the DEFAULT connection's dialect.
235
- // Per-connection dialect (when a user hits a non-default) is read from
236
- // the connection URL at query time by each call site that cares.
237
- 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));
238
418
 
239
- // Auto-run migrations on boot — EXCEPT when a CLI migration command
240
- // (`ream migrate` / `migrate:rollback` / `migrate:status`) booted us: it
241
- // drives migrations explicitly, so a boot-time pass would double-apply
242
- // (and silently re-apply right before a rollback/status). The CLI sets
243
- // `REAM_SKIP_BOOT_MIGRATE=1`; default (unset) preserves boot-migrate.
244
- if (config.migrations?.path && process.env.REAM_SKIP_BOOT_MIGRATE !== "1") {
245
- await this.#runMigrations(
246
- config.migrations.path,
247
- connections[defaultName]?.url,
248
- defaultConn,
249
- config.migrations.table,
250
- );
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;
251
451
  }
252
452
  }
253
453
 
@@ -261,6 +461,12 @@ export default class AtlasProvider {
261
461
  // - any rejection is aggregated into a single `AggregateError` thrown
262
462
  // at the end so supervisors / health-checks see a non-zero exit
263
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
+
264
470
  const named = [...this.#connections.entries()];
265
471
  const results = await Promise.allSettled(named.map(([, c]) => c.close()));
266
472
  this.#connections.clear();
@@ -268,9 +474,15 @@ export default class AtlasProvider {
268
474
  // Release the module-level singletons this provider populated at boot so a
269
475
  // re-boot starts clean and `db.*` can't dereference a now-closed handle.
270
476
  // `clearDb` is ownership-guarded, so clearing every connection only unbinds
271
- // the one still owning the singleton.
272
- const { clearDb } = await import("./services/db.js");
273
- 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
+ }
274
486
  clearCastRegistry();
275
487
  const errors = results
276
488
  .map((r, i) =>
@@ -309,23 +521,51 @@ export default class AtlasProvider {
309
521
 
310
522
  /** Normalize the config into a `{ name → ConnectionConfig }` map + default name. */
311
523
  #resolveConnections(config: AtlasDatabaseConfig): {
312
- connections: Record<string, ConnectionConfig>;
524
+ connections: Record<string, ConnectionConfig & { url: string }>;
313
525
  defaultName: string;
314
526
  } {
315
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
+ }
316
540
  return {
317
- connections: config.connections,
318
- defaultName: config.default ?? "primary",
541
+ connections,
542
+ // Lucid's `connection` (default selector); `default` is the alias.
543
+ defaultName: config.connection ?? config.default ?? "primary",
319
544
  };
320
545
  }
321
- // 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
+ }
322
557
  return {
323
558
  connections: {
324
559
  primary: {
325
560
  url: config.url,
326
561
  poolMin: config.poolMin,
327
562
  poolMax: config.poolMax,
563
+ pool: config.pool,
328
564
  pragmas: config.pragmas,
565
+ debug: config.debug,
566
+ connectRetries: config.connectRetries,
567
+ connectBackoffMs: config.connectBackoffMs,
568
+ connectTimeoutMs: config.connectTimeoutMs,
329
569
  },
330
570
  },
331
571
  defaultName: "primary",
@@ -337,6 +577,7 @@ export default class AtlasProvider {
337
577
  url: string,
338
578
  db: AsyncDatabaseConnection,
339
579
  tableName: string | undefined,
580
+ options?: { naturalSort?: boolean; disableTransactions?: boolean },
340
581
  ): Promise<void> {
341
582
  const { existsSync } = await import("node:fs");
342
583
  if (!existsSync(migrationsPath)) return;
@@ -353,11 +594,16 @@ export default class AtlasProvider {
353
594
  // `ream_migrations` bookkeeping row together). Without this, the runner
354
595
  // silently falls back to non-transactional execution.
355
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),
356
600
  };
357
601
  const runner = new MigrationRunner(adapter, {
358
602
  migrationsDir: migrationsPath,
359
603
  dialect: dialectFromUrl(url),
360
604
  tableName,
605
+ naturalSort: options?.naturalSort,
606
+ disableTransactions: options?.disableTransactions,
361
607
  });
362
608
  await runner.migrate();
363
609
  }
package/src/BaseEntity.ts CHANGED
@@ -139,6 +139,35 @@ export interface BelongsToRelationProxy extends BulkRelationProxy {
139
139
  */
140
140
  export interface ManyToManyRelationProxy extends BulkRelationProxy {
141
141
  readonly type: "manyToMany";
142
+ /**
143
+ * Persist a new related row AND its pivot link, optionally with pivot column
144
+ * values (Adonis Lucid `create(values, pivotAttributes)`).
145
+ */
146
+ create(
147
+ data: Record<string, unknown>,
148
+ pivotAttributes?: Record<string, unknown>,
149
+ ): Promise<BaseEntity>;
150
+ /** Persist an existing related instance + pivot link, with optional pivot values. */
151
+ save(
152
+ related: BaseEntity,
153
+ pivotAttributes?: Record<string, unknown>,
154
+ ): Promise<void>;
155
+ /** Bulk {@link create}; `pivotAttributes[i]` applies to `rows[i]`. */
156
+ createMany(
157
+ rows: Array<Record<string, unknown>>,
158
+ pivotAttributes?: Array<Record<string, unknown>>,
159
+ ): Promise<BaseEntity[]>;
160
+ /** Bulk {@link save}; `pivotAttributes[i]` applies to `related[i]`. */
161
+ saveMany(
162
+ related: BaseEntity[],
163
+ pivotAttributes?: Array<Record<string, unknown>>,
164
+ ): Promise<BaseEntity[]>;
165
+ /**
166
+ * A query builder on the PIVOT table itself, scoped to this parent (Adonis
167
+ * Lucid `pivotQuery`) — read/update/delete pivot rows directly, beyond
168
+ * attach/detach/sync. Returns the connection-level query builder.
169
+ */
170
+ pivotQuery(): import("./query/DatabaseQueryBuilder.js").DatabaseQueryBuilder;
142
171
  /** Insert pivot rows. Accepts `id[]` or `{ id: extras }`. */
143
172
  attach(
144
173
  ids: Array<string | number> | Record<string, Record<string, unknown>>,