@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.
- package/README.md +8 -0
- package/db.darwin-arm64.node +0 -0
- package/db.darwin-x64.node +0 -0
- package/db.linux-arm64-gnu.node +0 -0
- package/db.linux-x64-gnu.node +0 -0
- package/db.win32-x64-msvc.node +0 -0
- package/dist/AtlasProvider.d.ts +60 -7
- package/dist/AtlasProvider.d.ts.map +1 -1
- package/dist/AtlasProvider.js +204 -38
- package/dist/AtlasProvider.js.map +1 -1
- package/dist/BaseEntity.d.ts +17 -0
- package/dist/BaseEntity.d.ts.map +1 -1
- package/dist/BaseEntity.js.map +1 -1
- package/dist/BaseModel.d.ts +63 -27
- package/dist/BaseModel.d.ts.map +1 -1
- package/dist/BaseModel.js +108 -51
- package/dist/BaseModel.js.map +1 -1
- package/dist/BaseRepository.d.ts +11 -1
- package/dist/BaseRepository.d.ts.map +1 -1
- package/dist/BaseRepository.js +95 -20
- package/dist/BaseRepository.js.map +1 -1
- package/dist/ConnectionManager.d.ts +106 -0
- package/dist/ConnectionManager.d.ts.map +1 -0
- package/dist/ConnectionManager.js +228 -0
- package/dist/ConnectionManager.js.map +1 -0
- package/dist/ModelQuery.d.ts +256 -43
- package/dist/ModelQuery.d.ts.map +1 -1
- package/dist/ModelQuery.js +899 -231
- package/dist/ModelQuery.js.map +1 -1
- package/dist/Transaction.d.ts +54 -1
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +121 -59
- package/dist/Transaction.js.map +1 -1
- package/dist/adapters/NapiDbAdapter.d.ts +11 -0
- package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
- package/dist/adapters/NapiDbAdapter.js +35 -7
- package/dist/adapters/NapiDbAdapter.js.map +1 -1
- package/dist/console/contract.d.ts +62 -0
- package/dist/console/contract.d.ts.map +1 -0
- package/dist/console/contract.js +38 -0
- package/dist/console/contract.js.map +1 -0
- package/dist/console/factoryCommands.d.ts +23 -0
- package/dist/console/factoryCommands.d.ts.map +1 -0
- package/dist/console/factoryCommands.js +62 -0
- package/dist/console/factoryCommands.js.map +1 -0
- package/dist/console/migrationCommands.d.ts +38 -8
- package/dist/console/migrationCommands.d.ts.map +1 -1
- package/dist/console/migrationCommands.js +157 -50
- package/dist/console/migrationCommands.js.map +1 -1
- package/dist/console/schemaCheckCommand.d.ts +11 -17
- package/dist/console/schemaCheckCommand.d.ts.map +1 -1
- package/dist/console/schemaCheckCommand.js +21 -15
- package/dist/console/schemaCheckCommand.js.map +1 -1
- package/dist/console/schemaDumpCommand.d.ts +30 -0
- package/dist/console/schemaDumpCommand.d.ts.map +1 -0
- package/dist/console/schemaDumpCommand.js +69 -0
- package/dist/console/schemaDumpCommand.js.map +1 -0
- package/dist/console/schemaGenerateCommand.d.ts +100 -0
- package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
- package/dist/console/schemaGenerateCommand.js +246 -0
- package/dist/console/schemaGenerateCommand.js.map +1 -0
- package/dist/console/seederCommands.d.ts +46 -0
- package/dist/console/seederCommands.d.ts.map +1 -0
- package/dist/console/seederCommands.js +136 -0
- package/dist/console/seederCommands.js.map +1 -0
- package/dist/decorators/entity.d.ts +13 -2
- package/dist/decorators/entity.d.ts.map +1 -1
- package/dist/decorators/entity.js +10 -0
- package/dist/decorators/entity.js.map +1 -1
- package/dist/decorators/hooks.d.ts +3 -3
- package/dist/decorators/hooks.d.ts.map +1 -1
- package/dist/decorators/hooks.js.map +1 -1
- package/dist/events.d.ts +5 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/factories.d.ts +8 -0
- package/dist/factories.d.ts.map +1 -0
- package/dist/factories.js +8 -0
- package/dist/factories.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/lucid-schema.d.ts +15 -0
- package/dist/lucid-schema.d.ts.map +1 -0
- package/dist/lucid-schema.js +16 -0
- package/dist/lucid-schema.js.map +1 -0
- package/dist/orm.d.ts +14 -0
- package/dist/orm.d.ts.map +1 -0
- package/dist/orm.js +14 -0
- package/dist/orm.js.map +1 -0
- package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
- package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
- package/dist/query/DatabaseQueryBuilder.js +1798 -0
- package/dist/query/DatabaseQueryBuilder.js.map +1 -0
- package/dist/query/DmlBuilder.d.ts +62 -0
- package/dist/query/DmlBuilder.d.ts.map +1 -0
- package/dist/query/DmlBuilder.js +84 -0
- package/dist/query/DmlBuilder.js.map +1 -0
- package/dist/query/QueryBuilder.d.ts +5 -0
- package/dist/query/QueryBuilder.d.ts.map +1 -1
- package/dist/query/QueryBuilder.js +7 -0
- package/dist/query/QueryBuilder.js.map +1 -1
- package/dist/query/RawQueryBuilder.d.ts +47 -0
- package/dist/query/RawQueryBuilder.d.ts.map +1 -0
- package/dist/query/RawQueryBuilder.js +138 -0
- package/dist/query/RawQueryBuilder.js.map +1 -0
- package/dist/query/interpolate.d.ts +45 -0
- package/dist/query/interpolate.d.ts.map +1 -0
- package/dist/query/interpolate.js +51 -0
- package/dist/query/interpolate.js.map +1 -0
- package/dist/query/native.d.ts +8 -0
- package/dist/query/native.d.ts.map +1 -1
- package/dist/query/native.js +14 -0
- package/dist/query/native.js.map +1 -1
- package/dist/query/operators.d.ts +4 -0
- package/dist/query/operators.d.ts.map +1 -0
- package/dist/query/operators.js +27 -0
- package/dist/query/operators.js.map +1 -0
- package/dist/schema/Migration.d.ts +31 -7
- package/dist/schema/Migration.d.ts.map +1 -1
- package/dist/schema/Migration.js +34 -7
- package/dist/schema/Migration.js.map +1 -1
- package/dist/schema/MigrationRunner.d.ts +56 -6
- package/dist/schema/MigrationRunner.d.ts.map +1 -1
- package/dist/schema/MigrationRunner.js +382 -112
- package/dist/schema/MigrationRunner.js.map +1 -1
- package/dist/schema/Schema.d.ts +59 -2
- package/dist/schema/Schema.d.ts.map +1 -1
- package/dist/schema/Schema.js +143 -9
- package/dist/schema/Schema.js.map +1 -1
- package/dist/schema/SchemaDumper.d.ts +121 -0
- package/dist/schema/SchemaDumper.d.ts.map +1 -0
- package/dist/schema/SchemaDumper.js +365 -0
- package/dist/schema/SchemaDumper.js.map +1 -0
- package/dist/schema/Seeder.d.ts +13 -0
- package/dist/schema/Seeder.d.ts.map +1 -1
- package/dist/schema/Seeder.js +28 -5
- package/dist/schema/Seeder.js.map +1 -1
- package/dist/schema/TableBuilder.d.ts +27 -8
- package/dist/schema/TableBuilder.d.ts.map +1 -1
- package/dist/schema/TableBuilder.js +50 -12
- package/dist/schema/TableBuilder.js.map +1 -1
- package/dist/schema/catalog.d.ts +25 -1
- package/dist/schema/catalog.d.ts.map +1 -1
- package/dist/schema/catalog.js +69 -22
- package/dist/schema/catalog.js.map +1 -1
- package/dist/schema/introspect.d.ts +1 -1
- package/dist/schema/introspect.d.ts.map +1 -1
- package/dist/schema/introspect.js +26 -11
- package/dist/schema/introspect.js.map +1 -1
- package/dist/seeders.d.ts +7 -0
- package/dist/seeders.d.ts.map +1 -0
- package/dist/seeders.js +7 -0
- package/dist/seeders.js.map +1 -0
- package/dist/services/db.d.ts +120 -9
- package/dist/services/db.d.ts.map +1 -1
- package/dist/services/db.js +182 -26
- package/dist/services/db.js.map +1 -1
- package/dist/testing/DatabaseCleanup.d.ts +29 -6
- package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
- package/dist/testing/DatabaseCleanup.js +44 -17
- package/dist/testing/DatabaseCleanup.js.map +1 -1
- package/dist/testing/DbAssertions.d.ts +48 -0
- package/dist/testing/DbAssertions.d.ts.map +1 -0
- package/dist/testing/DbAssertions.js +80 -0
- package/dist/testing/DbAssertions.js.map +1 -0
- package/dist/testing/Factory.d.ts +111 -25
- package/dist/testing/Factory.d.ts.map +1 -1
- package/dist/testing/Factory.js +331 -56
- package/dist/testing/Factory.js.map +1 -1
- package/dist/testing/TestUtils.d.ts +49 -0
- package/dist/testing/TestUtils.d.ts.map +1 -0
- package/dist/testing/TestUtils.js +79 -0
- package/dist/testing/TestUtils.js.map +1 -0
- package/dist/testing/index.d.ts +3 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +3 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/types/relations.d.ts +31 -0
- package/dist/types/relations.d.ts.map +1 -0
- package/dist/types/relations.js +20 -0
- package/dist/types/relations.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +30 -2
- package/src/AtlasProvider.ts +297 -51
- package/src/BaseEntity.ts +29 -0
- package/src/BaseModel.ts +174 -37
- package/src/BaseRepository.ts +136 -17
- package/src/ConnectionManager.ts +298 -0
- package/src/ModelQuery.ts +1309 -260
- package/src/Transaction.ts +194 -60
- package/src/adapters/NapiDbAdapter.ts +72 -14
- package/src/console/contract.ts +96 -0
- package/src/console/factoryCommands.ts +73 -0
- package/src/console/migrationCommands.ts +253 -63
- package/src/console/schemaCheckCommand.ts +26 -22
- package/src/console/schemaDumpCommand.ts +96 -0
- package/src/console/schemaGenerateCommand.ts +380 -0
- package/src/console/seederCommands.ts +174 -0
- package/src/decorators/entity.ts +18 -2
- package/src/decorators/hooks.ts +5 -3
- package/src/events.ts +5 -0
- package/src/factories.ts +10 -0
- package/src/index.ts +50 -3
- package/src/lucid-schema.ts +22 -0
- package/src/orm.ts +13 -0
- package/src/query/DatabaseQueryBuilder.ts +2724 -0
- package/src/query/DmlBuilder.ts +131 -0
- package/src/query/QueryBuilder.ts +8 -0
- package/src/query/RawQueryBuilder.ts +173 -0
- package/src/query/interpolate.ts +73 -0
- package/src/query/native.ts +29 -0
- package/src/query/operators.ts +27 -0
- package/src/schema/Migration.ts +36 -7
- package/src/schema/MigrationRunner.ts +483 -119
- package/src/schema/Schema.ts +168 -19
- package/src/schema/SchemaDumper.ts +518 -0
- package/src/schema/Seeder.ts +47 -10
- package/src/schema/TableBuilder.ts +48 -11
- package/src/schema/catalog.ts +94 -24
- package/src/schema/introspect.ts +26 -9
- package/src/seeders.ts +16 -0
- package/src/services/db.ts +343 -31
- package/src/testing/DatabaseCleanup.ts +73 -21
- package/src/testing/DbAssertions.ts +127 -0
- package/src/testing/Factory.ts +559 -79
- package/src/testing/TestUtils.ts +123 -0
- package/src/testing/index.ts +12 -1
- package/src/types/relations.ts +43 -0
package/src/AtlasProvider.ts
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* }
|
|
18
18
|
*
|
|
19
19
|
* // Code
|
|
20
|
-
* const primary = app.container.resolve('db')
|
|
21
|
-
* const tenant1 = app.container.resolve('db:tenant1')
|
|
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
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
config.migrations
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
|
318
|
-
|
|
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
|
|
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>>,
|