@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/schema/catalog.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* they are framework introspection, not user queries, so they stay raw SQL.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { AtlasError } from "../errors.js";
|
|
10
11
|
import type { AtlasDialect } from "../query/native.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -22,6 +23,20 @@ export interface CatalogConnection {
|
|
|
22
23
|
// Return value is ignored — the two adapter shapes disagree on it
|
|
23
24
|
// (`void` vs `{ rowsAffected }`), and these helpers only run it for effect.
|
|
24
25
|
execute(sql: string, params?: unknown[]): Promise<unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Optional: run a batch of statements atomically on ONE pinned pooled
|
|
28
|
+
* connection. When present, {@link runWithoutForeignKeys} uses it so the FK
|
|
29
|
+
* toggle and the operations can't scatter across the pool.
|
|
30
|
+
*/
|
|
31
|
+
runInTransaction?(
|
|
32
|
+
batch: readonly { sql: string; params?: unknown[] }[],
|
|
33
|
+
): Promise<number>;
|
|
34
|
+
/**
|
|
35
|
+
* Optional MANAGED interactive transaction pinned to ONE connection (Lucid
|
|
36
|
+
* `db.transaction(cb)`). When present, {@link runWithoutForeignKeys} uses it
|
|
37
|
+
* on MySQL so the FK restore runs in a real `finally` on that same connection.
|
|
38
|
+
*/
|
|
39
|
+
transaction?<T>(callback: (trx: CatalogConnection) => Promise<T>): Promise<T>;
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
/**
|
|
@@ -37,6 +52,12 @@ export interface ListTablesOptions {
|
|
|
37
52
|
* because it re-creates the tracking table afterwards.
|
|
38
53
|
*/
|
|
39
54
|
includeFrameworkTables?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* PostgreSQL only — restrict the listing to these schemas instead of just the
|
|
57
|
+
* current one (Adonis Lucid `schemaGeneration.schemas`). Ignored on
|
|
58
|
+
* sqlite/mysql, which have no schema namespace of this kind.
|
|
59
|
+
*/
|
|
60
|
+
schemas?: string[];
|
|
40
61
|
}
|
|
41
62
|
|
|
42
63
|
/** List every base table in the current schema/database, for `dialect`. */
|
|
@@ -46,21 +67,33 @@ export async function listUserTables(
|
|
|
46
67
|
options: ListTablesOptions = {},
|
|
47
68
|
): Promise<string[]> {
|
|
48
69
|
let sql: string;
|
|
70
|
+
const params: unknown[] = [];
|
|
49
71
|
switch (dialect) {
|
|
50
72
|
case "sqlite":
|
|
51
73
|
sql =
|
|
52
74
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
|
|
53
75
|
break;
|
|
54
76
|
case "postgres":
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
// Restrict to the given schemas (Lucid `schemas`) or the current one.
|
|
78
|
+
// Bind each schema as its own scalar placeholder — atlas's driver binds
|
|
79
|
+
// arrays as text, so `schemaname = ANY($1)` would fail; an IN list works.
|
|
80
|
+
if (options.schemas && options.schemas.length > 0) {
|
|
81
|
+
const placeholders = options.schemas.map((_, i) => `$${i + 1}`);
|
|
82
|
+
params.push(...options.schemas);
|
|
83
|
+
sql = `SELECT tablename AS name FROM pg_tables WHERE schemaname IN (${placeholders.join(", ")})`;
|
|
84
|
+
} else {
|
|
85
|
+
sql =
|
|
86
|
+
"SELECT tablename AS name FROM pg_tables WHERE schemaname = current_schema()";
|
|
87
|
+
}
|
|
57
88
|
break;
|
|
58
89
|
case "mysql":
|
|
90
|
+
// CAST to CHAR: MySQL returns information_schema identifier columns as
|
|
91
|
+
// binary (Uint8Array), which the string filter below would drop.
|
|
59
92
|
sql =
|
|
60
|
-
"SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'";
|
|
93
|
+
"SELECT CAST(table_name AS CHAR) AS name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'";
|
|
61
94
|
break;
|
|
62
95
|
}
|
|
63
|
-
const rows = await db.query<{ name: unknown }>(sql);
|
|
96
|
+
const rows = await db.query<{ name: unknown }>(sql, params);
|
|
64
97
|
const names: string[] = [];
|
|
65
98
|
for (const row of rows) {
|
|
66
99
|
if (typeof row.name !== "string") continue;
|
|
@@ -139,28 +172,65 @@ export async function columnExists(
|
|
|
139
172
|
* order. Postgres has no session-level switch (it uses `TRUNCATE … CASCADE` /
|
|
140
173
|
* `DROP … CASCADE` instead), so this is a no-op there.
|
|
141
174
|
*/
|
|
142
|
-
export async function
|
|
175
|
+
export async function runWithoutForeignKeys(
|
|
143
176
|
db: CatalogConnection,
|
|
144
177
|
dialect: AtlasDialect,
|
|
145
|
-
|
|
146
|
-
): Promise<
|
|
147
|
-
|
|
148
|
-
dialect === "sqlite"
|
|
149
|
-
? "PRAGMA foreign_keys = OFF"
|
|
150
|
-
: dialect === "mysql"
|
|
151
|
-
? "SET FOREIGN_KEY_CHECKS = 0"
|
|
152
|
-
: null;
|
|
153
|
-
const on =
|
|
154
|
-
dialect === "sqlite"
|
|
155
|
-
? "PRAGMA foreign_keys = ON"
|
|
156
|
-
: dialect === "mysql"
|
|
157
|
-
? "SET FOREIGN_KEY_CHECKS = 1"
|
|
158
|
-
: null;
|
|
178
|
+
statements: ReadonlyArray<{ sql: string; params?: unknown[] }>,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
if (statements.length === 0) return;
|
|
159
181
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
182
|
+
// MySQL: `SET FOREIGN_KEY_CHECKS` is a SESSION variable (not transactional),
|
|
183
|
+
// so it MUST be restored in a real `finally` on the SAME pinned connection —
|
|
184
|
+
// otherwise a mid-batch failure leaves the pooled connection with FK checks
|
|
185
|
+
// off. Use the managed interactive transaction: SET 0 → ops → finally SET 1,
|
|
186
|
+
// all on the one pinned connection, restore guaranteed even on error.
|
|
187
|
+
if (dialect === "mysql") {
|
|
188
|
+
// MySQL `SET FOREIGN_KEY_CHECKS` is a SESSION variable (not transactional),
|
|
189
|
+
// so it MUST be restored in a real `finally` on the SAME pinned connection.
|
|
190
|
+
// That needs the managed interactive transaction: SET 0 → ops → finally
|
|
191
|
+
// SET 1, restore guaranteed even on error. Without transaction() there is
|
|
192
|
+
// no safe restore — a mid-batch failure would leave the pooled connection
|
|
193
|
+
// with FK checks OFF (a poisoned connection). Refuse rather than corrupt.
|
|
194
|
+
if (!db.transaction) {
|
|
195
|
+
throw new AtlasError(
|
|
196
|
+
"E_MYSQL_FK_REQUIRES_TRANSACTION",
|
|
197
|
+
"Suspending MySQL foreign-key checks needs an interactive transaction() to restore them in a finally — a session-level SET can't be rolled back, so a failure mid-batch would leave the pooled connection with FK checks disabled. This adapter has none.",
|
|
198
|
+
{
|
|
199
|
+
hint: "Use a real connection (createNapiConnection) or an adapter that implements transaction().",
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
await db.transaction(async (trx) => {
|
|
204
|
+
try {
|
|
205
|
+
await trx.execute("SET FOREIGN_KEY_CHECKS = 0");
|
|
206
|
+
for (const stmt of statements) {
|
|
207
|
+
await trx.execute(stmt.sql, stmt.params);
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
await trx.execute("SET FOREIGN_KEY_CHECKS = 1");
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// sqlite/postgres: run the FK toggle + ops on ONE pinned connection via
|
|
217
|
+
// runInTransaction so a pool can't scatter the connection-local toggle away
|
|
218
|
+
// from the ops.
|
|
219
|
+
// - sqlite: `PRAGMA defer_foreign_keys` is transaction-scoped and auto-resets
|
|
220
|
+
// at txn end (no leak); plain `PRAGMA foreign_keys` is ignored in a txn;
|
|
221
|
+
// - postgres: no session toggle — drops use CASCADE and `truncateAll` uses
|
|
222
|
+
// `TRUNCATE … CASCADE`, so pg statements are already FK-safe.
|
|
223
|
+
const batch: Array<{ sql: string; params?: unknown[] }> = [];
|
|
224
|
+
if (dialect === "sqlite") {
|
|
225
|
+
batch.push({ sql: "PRAGMA defer_foreign_keys = ON" });
|
|
226
|
+
}
|
|
227
|
+
batch.push(...statements);
|
|
228
|
+
if (db.runInTransaction) {
|
|
229
|
+
await db.runInTransaction(batch);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// No transaction support at all (a bare fake): best effort, single connection.
|
|
233
|
+
for (const stmt of batch) {
|
|
234
|
+
await db.execute(stmt.sql, stmt.params);
|
|
165
235
|
}
|
|
166
236
|
}
|
package/src/schema/introspect.ts
CHANGED
|
@@ -49,15 +49,17 @@ export async function introspectTable(
|
|
|
49
49
|
db: SchemaIntrospectable,
|
|
50
50
|
dialect: AtlasDialect,
|
|
51
51
|
table: string,
|
|
52
|
+
schema?: string,
|
|
52
53
|
): Promise<IntrospectedColumn[] | null> {
|
|
53
54
|
assertIdent(table);
|
|
55
|
+
if (schema !== undefined) assertIdent(schema);
|
|
54
56
|
switch (dialect) {
|
|
55
57
|
case "sqlite":
|
|
56
58
|
return introspectSqlite(db, table);
|
|
57
59
|
case "postgres":
|
|
58
|
-
return introspectPostgres(db, table);
|
|
60
|
+
return introspectPostgres(db, table, schema);
|
|
59
61
|
case "mysql":
|
|
60
|
-
return introspectMysql(db, table);
|
|
62
|
+
return introspectMysql(db, table, schema);
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -79,13 +81,17 @@ async function introspectSqlite(
|
|
|
79
81
|
async function introspectPostgres(
|
|
80
82
|
db: SchemaIntrospectable,
|
|
81
83
|
table: string,
|
|
84
|
+
schema?: string,
|
|
82
85
|
): Promise<IntrospectedColumn[] | null> {
|
|
86
|
+
// Target a specific schema when given (Lucid `schemas`), else the current one.
|
|
87
|
+
const schemaPred = schema ? "$2" : "current_schema()";
|
|
88
|
+
const args = schema ? [table, schema] : [table];
|
|
83
89
|
const cols = await db.query(
|
|
84
90
|
`SELECT column_name, data_type, is_nullable, column_default
|
|
85
91
|
FROM information_schema.columns
|
|
86
|
-
WHERE table_schema =
|
|
92
|
+
WHERE table_schema = ${schemaPred} AND table_name = $1
|
|
87
93
|
ORDER BY ordinal_position`,
|
|
88
|
-
|
|
94
|
+
args,
|
|
89
95
|
);
|
|
90
96
|
if (cols.length === 0) return null;
|
|
91
97
|
const pkRows = await db.query(
|
|
@@ -94,8 +100,9 @@ async function introspectPostgres(
|
|
|
94
100
|
JOIN information_schema.key_column_usage kcu
|
|
95
101
|
ON kcu.constraint_name = tc.constraint_name
|
|
96
102
|
AND kcu.table_schema = tc.table_schema
|
|
97
|
-
WHERE tc.table_name = $1 AND tc.constraint_type = 'PRIMARY KEY'
|
|
98
|
-
|
|
103
|
+
WHERE tc.table_name = $1 AND tc.constraint_type = 'PRIMARY KEY'
|
|
104
|
+
AND tc.table_schema = ${schemaPred}`,
|
|
105
|
+
args,
|
|
99
106
|
);
|
|
100
107
|
const pks = new Set(pkRows.map((r) => String(r.column_name)));
|
|
101
108
|
return cols.map((r) => ({
|
|
@@ -110,13 +117,23 @@ async function introspectPostgres(
|
|
|
110
117
|
async function introspectMysql(
|
|
111
118
|
db: SchemaIntrospectable,
|
|
112
119
|
table: string,
|
|
120
|
+
schema?: string,
|
|
113
121
|
): Promise<IntrospectedColumn[] | null> {
|
|
122
|
+
// In MySQL a "schema" IS a database; target it when given, else DATABASE().
|
|
123
|
+
const schemaPred = schema ? "?" : "DATABASE()";
|
|
124
|
+
const args = schema ? [schema, table] : [table];
|
|
125
|
+
// CAST the identifier/enum columns to CHAR: MySQL returns information_schema
|
|
126
|
+
// text as binary (Uint8Array), which `String(...)` would turn into byte lists.
|
|
114
127
|
const rows = await db.query(
|
|
115
|
-
`SELECT column_name
|
|
128
|
+
`SELECT CAST(column_name AS CHAR) AS column_name,
|
|
129
|
+
CAST(data_type AS CHAR) AS data_type,
|
|
130
|
+
CAST(is_nullable AS CHAR) AS is_nullable,
|
|
131
|
+
column_default,
|
|
132
|
+
CAST(column_key AS CHAR) AS column_key
|
|
116
133
|
FROM information_schema.columns
|
|
117
|
-
WHERE table_schema =
|
|
134
|
+
WHERE table_schema = ${schemaPred} AND table_name = ?
|
|
118
135
|
ORDER BY ordinal_position`,
|
|
119
|
-
|
|
136
|
+
args,
|
|
120
137
|
);
|
|
121
138
|
if (rows.length === 0) return null;
|
|
122
139
|
return rows.map((r) => ({
|
package/src/seeders.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/atlas/seeders` — Adonis Lucid seeders parity subpath. The `BaseSeeder`
|
|
3
|
+
* base class plus the runners and the `make:seeder` / `db:seed` console commands.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
dbSeedCommand,
|
|
8
|
+
makeSeederCommand,
|
|
9
|
+
type SeederCommandOptions,
|
|
10
|
+
} from "./console/seederCommands.js";
|
|
11
|
+
export {
|
|
12
|
+
BaseSeeder,
|
|
13
|
+
runSeederDirectory,
|
|
14
|
+
runSeeders,
|
|
15
|
+
Seeder,
|
|
16
|
+
} from "./schema/Seeder.js";
|
package/src/services/db.ts
CHANGED
|
@@ -13,18 +13,133 @@
|
|
|
13
13
|
* container hooks).
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import type { ConnectionConfig } from "../AtlasProvider.js";
|
|
16
17
|
import type { AsyncDatabaseConnection } from "../adapters/NapiDbAdapter.js";
|
|
18
|
+
import { ConnectionManager } from "../ConnectionManager.js";
|
|
19
|
+
import {
|
|
20
|
+
DatabaseQueryBuilder,
|
|
21
|
+
type QueryExecutor,
|
|
22
|
+
} from "../query/DatabaseQueryBuilder.js";
|
|
23
|
+
import type { AtlasDialect } from "../query/native.js";
|
|
17
24
|
import { RawSql } from "../query/QueryBuilder.js";
|
|
25
|
+
import {
|
|
26
|
+
RawQueryBuilder,
|
|
27
|
+
resolveRawBindings,
|
|
28
|
+
} from "../query/RawQueryBuilder.js";
|
|
29
|
+
import { truncateAll } from "../testing/DatabaseCleanup.js";
|
|
18
30
|
|
|
19
|
-
/**
|
|
20
|
-
export interface
|
|
31
|
+
/** Options accepted by the Lucid query-builder entry points. */
|
|
32
|
+
export interface DbQueryOptions {
|
|
33
|
+
/** Route the query through this transaction client (Lucid `{ client: trx }`). */
|
|
34
|
+
client?: QueryExecutor;
|
|
35
|
+
/** `'read'` rejects writes on this builder (Lucid `db.query({ mode: 'read' })`). */
|
|
36
|
+
mode?: "read" | "write";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Options for {@link DbService.connection} (Lucid read/write replica routing). */
|
|
40
|
+
export interface ConnectionOptions {
|
|
21
41
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
42
|
+
* `'read'` scopes the returned service to reads: its query builders reject
|
|
43
|
+
* writes (insert/update/delete/increment/decrement). `'write'` (default) is
|
|
44
|
+
* unrestricted. Atlas has no replica pool, so `mode` is a write-guard rather
|
|
45
|
+
* than a routing hint — the guard is the security-relevant half of Lucid's
|
|
46
|
+
* read/write modes.
|
|
47
|
+
*/
|
|
48
|
+
mode?: "read" | "write";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The `db` service surface — Adonis Lucid's `Database` service. Exposes the
|
|
53
|
+
* query builders (`query`/`from`/`table`/`insertQuery`), raw execution
|
|
54
|
+
* (`rawQuery`), the `raw()` fragment builder, connection scoping (`connection`),
|
|
55
|
+
* and the transaction/DDL methods forwarded from the bound connection.
|
|
56
|
+
*/
|
|
57
|
+
export interface DbService {
|
|
58
|
+
/** A connection-level query builder (Lucid `db.query()`), optionally on a trx. */
|
|
59
|
+
query(options?: DbQueryOptions): DatabaseQueryBuilder;
|
|
60
|
+
/** Query builder with the table pre-selected (Lucid `db.from(table)`). */
|
|
61
|
+
from(table: string): DatabaseQueryBuilder;
|
|
62
|
+
/**
|
|
63
|
+
* Query builder on a derived-table source (Lucid `db.from(subquery, alias)`) —
|
|
64
|
+
* a builder OR a callback that builds one.
|
|
65
|
+
*/
|
|
66
|
+
from(
|
|
67
|
+
subquery: DatabaseQueryBuilder | ((query: DatabaseQueryBuilder) => void),
|
|
68
|
+
alias?: string,
|
|
69
|
+
): DatabaseQueryBuilder;
|
|
70
|
+
/** Insert/write builder with the table pre-selected (Lucid `db.table(table)`). */
|
|
71
|
+
table(table: string): DatabaseQueryBuilder;
|
|
72
|
+
/** An insert builder (Lucid `db.insertQuery()`), optionally on a trx. */
|
|
73
|
+
insertQuery(options?: DbQueryOptions): DatabaseQueryBuilder;
|
|
74
|
+
/**
|
|
75
|
+
* A chainable raw query (Lucid `db.rawQuery(sql, bindings)`). Thenable — can be
|
|
76
|
+
* awaited directly — and exposes `toSQL`/`toQuery`/`debug`/`timeout`/
|
|
77
|
+
* `reporterData`. Bindings may be positional (`?`/`??`) or named (`:name`/`:name:`).
|
|
78
|
+
*/
|
|
79
|
+
rawQuery<T = Record<string, unknown>>(
|
|
80
|
+
sql: string,
|
|
81
|
+
bindings?: unknown[] | Record<string, unknown>,
|
|
82
|
+
): RawQueryBuilder<T>;
|
|
83
|
+
/**
|
|
84
|
+
* Scope the service to a connection (Lucid `db.connection(name)`). Called with
|
|
85
|
+
* no name it returns the default connection's service (Lucid `db.connection()`).
|
|
86
|
+
* Pass `{ mode: 'read' }` to reject writes on the returned service (Lucid
|
|
87
|
+
* `db.connection(name, { mode: 'read' })`).
|
|
88
|
+
*/
|
|
89
|
+
connection(name?: string, options?: ConnectionOptions): DbService;
|
|
90
|
+
/**
|
|
91
|
+
* Query builder for a model whose class is resolved at runtime (Lucid
|
|
92
|
+
* `db.modelQuery(Model)`). For static code prefer `Model.query()` directly.
|
|
93
|
+
*/
|
|
94
|
+
modelQuery<Q>(model: { query(): Q }): Q;
|
|
95
|
+
/**
|
|
96
|
+
* Build a raw SQL expression — AdonisJS `db.raw()`. For query fragments and
|
|
97
|
+
* column defaults that are SQL expressions:
|
|
24
98
|
*
|
|
25
99
|
* t.uuid('id').defaultTo(db.raw('gen_random_uuid()'))
|
|
100
|
+
*
|
|
101
|
+
* Bindings may be positional (`?`/`??`) or named (`:name`/`:name:`).
|
|
102
|
+
*/
|
|
103
|
+
raw(sql: string, params?: unknown[] | Record<string, unknown>): RawSql;
|
|
104
|
+
/**
|
|
105
|
+
* A column reference — Adonis Lucid `db.ref('posts.created_at')`. Use it where
|
|
106
|
+
* a value position must be read as a column (e.g. `orderBy(db.ref(col), 'desc')`).
|
|
107
|
+
* The identifier is validated and dialect-quoted; it is NOT a value binding.
|
|
108
|
+
*/
|
|
109
|
+
ref(column: string): RawSql;
|
|
110
|
+
/** Run a statement for effect (forwarded to the connection). */
|
|
111
|
+
execute(sql: string, params?: unknown[]): Promise<{ rowsAffected: number }>;
|
|
112
|
+
/** Managed/manual interactive transaction (forwarded, Lucid `db.transaction`). */
|
|
113
|
+
transaction: AsyncDatabaseConnection["transaction"];
|
|
114
|
+
/** Atomic batch (forwarded). */
|
|
115
|
+
runInTransaction: AsyncDatabaseConnection["runInTransaction"];
|
|
116
|
+
/**
|
|
117
|
+
* Empty a table (Lucid `truncate`). `TRUNCATE TABLE` on Postgres/MySQL (with
|
|
118
|
+
* `CASCADE` when `cascade` is set, Postgres only); `DELETE FROM` on SQLite,
|
|
119
|
+
* which has no `TRUNCATE`.
|
|
26
120
|
*/
|
|
27
|
-
|
|
121
|
+
truncate(table: string, cascade?: boolean): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Empty every user table (Lucid `truncateAllTables`). Framework tables
|
|
124
|
+
* (`ream_*`) and dialect internals are left alone; pass `ignoreTables` to spare
|
|
125
|
+
* more. Foreign keys are suspended so delete order doesn't matter.
|
|
126
|
+
*/
|
|
127
|
+
truncateAllTables(ignoreTables?: readonly string[]): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Try to acquire a session-level advisory lock, non-blocking (Lucid
|
|
130
|
+
* `getAdvisoryLock`). Postgres `pg_try_advisory_lock`, MySQL `GET_LOCK(key, 0)`.
|
|
131
|
+
* A string key is hashed to the integer Postgres requires. Returns whether the
|
|
132
|
+
* lock was acquired. **Throws on SQLite** (no advisory locks — Lucid parity).
|
|
133
|
+
*/
|
|
134
|
+
getAdvisoryLock(key: string | number): Promise<boolean>;
|
|
135
|
+
/** Release an advisory lock taken with {@link getAdvisoryLock}. Throws on SQLite. */
|
|
136
|
+
releaseAdvisoryLock(key: string | number): Promise<boolean>;
|
|
137
|
+
/** The full Lucid connection manager (`add`/`connect`/`patch`/`release`/nodes/events). */
|
|
138
|
+
readonly manager: ConnectionManager;
|
|
139
|
+
/** The bound connection's dialect. */
|
|
140
|
+
readonly dialect: AtlasDialect;
|
|
141
|
+
ping(): Promise<void>;
|
|
142
|
+
close(): Promise<void>;
|
|
28
143
|
}
|
|
29
144
|
|
|
30
145
|
let instance: AsyncDatabaseConnection | undefined;
|
|
@@ -49,51 +164,248 @@ export function getDb(): AsyncDatabaseConnection | undefined {
|
|
|
49
164
|
return instance;
|
|
50
165
|
}
|
|
51
166
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
const
|
|
167
|
+
// The shared connection manager (Lucid `db.manager`) — the single owner of named
|
|
168
|
+
// connections. Backs `BaseModel.connection = 'analytics'` so a model resolves a
|
|
169
|
+
// non-default connection from a plain import (AdonisJS `static connection`).
|
|
170
|
+
const manager = new ConnectionManager();
|
|
56
171
|
|
|
57
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* @internal Register an already-open named connection (called by AtlasProvider,
|
|
174
|
+
* which opens connections itself). Records the config on the node too.
|
|
175
|
+
*/
|
|
58
176
|
export function registerConnection(
|
|
59
177
|
name: string,
|
|
60
178
|
connection: AsyncDatabaseConnection,
|
|
179
|
+
config: ConnectionConfig = {},
|
|
61
180
|
): void {
|
|
62
|
-
|
|
181
|
+
manager.register(name, config, connection);
|
|
63
182
|
}
|
|
64
183
|
|
|
65
|
-
/** @internal Unregister a named connection IF it still points at `connection
|
|
184
|
+
/** @internal Unregister a named connection IF it still points at `connection` (no close). */
|
|
66
185
|
export function unregisterConnection(
|
|
67
186
|
name: string,
|
|
68
187
|
connection: AsyncDatabaseConnection,
|
|
69
188
|
): void {
|
|
70
|
-
|
|
189
|
+
manager.deregister(name, connection);
|
|
71
190
|
}
|
|
72
191
|
|
|
73
|
-
/** @internal Resolve a named connection (for `BaseModel.connection`), or `undefined`. */
|
|
192
|
+
/** @internal Resolve a live named connection (for `BaseModel.connection`), or `undefined`. */
|
|
74
193
|
export function getConnection(
|
|
75
194
|
name: string,
|
|
76
195
|
): AsyncDatabaseConnection | undefined {
|
|
77
|
-
return
|
|
196
|
+
return manager.connection(name);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @internal The shared connection manager (for the AtlasProvider lifecycle-event bridge). */
|
|
200
|
+
export function connectionManager(): ConnectionManager {
|
|
201
|
+
return manager;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Coerce an advisory-lock key to the integer Postgres `pg_*_advisory_lock`
|
|
206
|
+
* requires. Numeric keys pass through; string keys are hashed deterministically
|
|
207
|
+
* (FNV-1a, 32-bit) so lock and unlock agree. Stable within a process is all that
|
|
208
|
+
* matters — atlas never shares a lock namespace with Knex.
|
|
209
|
+
*/
|
|
210
|
+
function advisoryLockKey(key: string | number): number {
|
|
211
|
+
if (typeof key === "number") return Math.trunc(key);
|
|
212
|
+
let hash = 0x811c9dc5;
|
|
213
|
+
for (let i = 0; i < key.length; i++) {
|
|
214
|
+
hash ^= key.charCodeAt(i);
|
|
215
|
+
hash = Math.imul(hash, 0x01000193);
|
|
216
|
+
}
|
|
217
|
+
return hash | 0; // signed 32-bit — fits Postgres int/bigint
|
|
78
218
|
}
|
|
79
219
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
220
|
+
/** Validate + dialect-quote a (dot-qualified) identifier. Rejects anything unsafe. */
|
|
221
|
+
function quoteIdent(name: string, dialect: AtlasDialect): string {
|
|
222
|
+
const q = dialect === "mysql" ? "`" : '"';
|
|
223
|
+
return name
|
|
224
|
+
.split(".")
|
|
225
|
+
.map((seg) => {
|
|
226
|
+
if (seg === "*") return seg;
|
|
227
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`atlas: invalid identifier segment '${seg}' in '${name}'`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return `${q}${seg}${q}`;
|
|
233
|
+
})
|
|
234
|
+
.join(".");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Build a {@link DbService} over a resolver that yields the live connection. */
|
|
238
|
+
export function createDbService(
|
|
239
|
+
resolve: () => AsyncDatabaseConnection,
|
|
240
|
+
readOnly = false,
|
|
241
|
+
): DbService {
|
|
242
|
+
const opts = readOnly ? { readOnly } : undefined;
|
|
243
|
+
// Per-call `{ mode: 'read' }` also yields a write-guarded builder, even on an
|
|
244
|
+
// unscoped service (Lucid `db.query({ mode: 'read' })`).
|
|
245
|
+
const optsFor = (options?: DbQueryOptions) =>
|
|
246
|
+
readOnly || options?.mode === "read" ? { readOnly: true } : undefined;
|
|
247
|
+
return {
|
|
248
|
+
query(options) {
|
|
249
|
+
const conn = resolve();
|
|
250
|
+
return new DatabaseQueryBuilder(
|
|
251
|
+
options?.client ?? conn,
|
|
252
|
+
conn.dialect,
|
|
253
|
+
"",
|
|
254
|
+
optsFor(options),
|
|
255
|
+
);
|
|
256
|
+
},
|
|
257
|
+
from(
|
|
258
|
+
source:
|
|
259
|
+
| string
|
|
260
|
+
| DatabaseQueryBuilder
|
|
261
|
+
| ((query: DatabaseQueryBuilder) => void),
|
|
262
|
+
alias?: string,
|
|
263
|
+
) {
|
|
264
|
+
const conn = resolve();
|
|
265
|
+
const builder = new DatabaseQueryBuilder(conn, conn.dialect, "", opts);
|
|
266
|
+
return typeof source === "string"
|
|
267
|
+
? builder.from(source)
|
|
268
|
+
: builder.from(source, alias);
|
|
269
|
+
},
|
|
270
|
+
table(table) {
|
|
271
|
+
const conn = resolve();
|
|
272
|
+
return new DatabaseQueryBuilder(conn, conn.dialect, table, opts);
|
|
273
|
+
},
|
|
274
|
+
insertQuery(options) {
|
|
275
|
+
const conn = resolve();
|
|
276
|
+
return new DatabaseQueryBuilder(
|
|
277
|
+
options?.client ?? conn,
|
|
278
|
+
conn.dialect,
|
|
279
|
+
"",
|
|
280
|
+
optsFor(options),
|
|
92
281
|
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
282
|
+
},
|
|
283
|
+
rawQuery(sql, bindings = []) {
|
|
284
|
+
const conn = resolve();
|
|
285
|
+
return new RawQueryBuilder(conn, conn.dialect, sql, bindings);
|
|
286
|
+
},
|
|
287
|
+
connection(name, connOptions) {
|
|
288
|
+
// No name → the default connection's service (Lucid `db.connection()`).
|
|
289
|
+
if (name === undefined) {
|
|
290
|
+
return createDbService(resolve, connOptions?.mode === "read");
|
|
291
|
+
}
|
|
292
|
+
return createDbService(() => {
|
|
293
|
+
const conn = getConnection(name);
|
|
294
|
+
if (!conn) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`[atlas] no connection registered under '${name}'. Is it in config/database.ts connections?`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return conn;
|
|
300
|
+
}, connOptions?.mode === "read");
|
|
301
|
+
},
|
|
302
|
+
modelQuery(model) {
|
|
303
|
+
return model.query();
|
|
304
|
+
},
|
|
305
|
+
raw(sql, params = []) {
|
|
306
|
+
// Resolve `??`/named bindings only when present, so the common
|
|
307
|
+
// positional/no-binding path (and Postgres `::casts`) is untouched.
|
|
308
|
+
const hasNamed = !Array.isArray(params);
|
|
309
|
+
const hasIdent = typeof sql === "string" && sql.includes("??");
|
|
310
|
+
if (!hasNamed && !hasIdent) {
|
|
311
|
+
return new RawSql(sql, params as unknown[]);
|
|
312
|
+
}
|
|
313
|
+
const resolved = resolveRawBindings(sql, params, resolve().dialect);
|
|
314
|
+
return new RawSql(resolved.sql, resolved.params);
|
|
315
|
+
},
|
|
316
|
+
ref(column) {
|
|
317
|
+
return new RawSql(quoteIdent(column, resolve().dialect), []);
|
|
318
|
+
},
|
|
319
|
+
async truncate(table, cascade) {
|
|
320
|
+
const conn = resolve();
|
|
321
|
+
const t = quoteIdent(table, conn.dialect);
|
|
322
|
+
// SQLite has no TRUNCATE — DELETE clears the table (Lucid does the same).
|
|
323
|
+
const sql =
|
|
324
|
+
conn.dialect === "sqlite"
|
|
325
|
+
? `DELETE FROM ${t}`
|
|
326
|
+
: conn.dialect === "postgres"
|
|
327
|
+
? `TRUNCATE TABLE ${t}${cascade ? " CASCADE" : ""}`
|
|
328
|
+
: `TRUNCATE TABLE ${t}`;
|
|
329
|
+
await conn.execute(sql, []);
|
|
330
|
+
},
|
|
331
|
+
truncateAllTables(ignoreTables = []) {
|
|
332
|
+
return truncateAll(resolve(), ignoreTables);
|
|
333
|
+
},
|
|
334
|
+
async getAdvisoryLock(key) {
|
|
335
|
+
const conn = resolve();
|
|
336
|
+
if (conn.dialect === "sqlite") {
|
|
337
|
+
throw new Error(
|
|
338
|
+
"[atlas] advisory locks are not supported on SQLite (Postgres/MySQL only).",
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (conn.dialect === "postgres") {
|
|
342
|
+
const rows = await conn.query<{ locked: boolean }>(
|
|
343
|
+
"SELECT pg_try_advisory_lock($1) AS locked",
|
|
344
|
+
[advisoryLockKey(key)],
|
|
345
|
+
);
|
|
346
|
+
return Boolean(rows[0]?.locked);
|
|
347
|
+
}
|
|
348
|
+
// MySQL: GET_LOCK(name, 0) → non-blocking try (1 acquired, 0 busy).
|
|
349
|
+
const rows = await conn.query<{ locked: number }>(
|
|
350
|
+
"SELECT GET_LOCK(?, 0) AS locked",
|
|
351
|
+
[String(key)],
|
|
352
|
+
);
|
|
353
|
+
return Number(rows[0]?.locked) === 1;
|
|
354
|
+
},
|
|
355
|
+
async releaseAdvisoryLock(key) {
|
|
356
|
+
const conn = resolve();
|
|
357
|
+
if (conn.dialect === "sqlite") {
|
|
358
|
+
throw new Error(
|
|
359
|
+
"[atlas] advisory locks are not supported on SQLite (Postgres/MySQL only).",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (conn.dialect === "postgres") {
|
|
363
|
+
const rows = await conn.query<{ released: boolean }>(
|
|
364
|
+
"SELECT pg_advisory_unlock($1) AS released",
|
|
365
|
+
[advisoryLockKey(key)],
|
|
366
|
+
);
|
|
367
|
+
return Boolean(rows[0]?.released);
|
|
368
|
+
}
|
|
369
|
+
const rows = await conn.query<{ released: number }>(
|
|
370
|
+
"SELECT RELEASE_LOCK(?) AS released",
|
|
371
|
+
[String(key)],
|
|
372
|
+
);
|
|
373
|
+
return Number(rows[0]?.released) === 1;
|
|
374
|
+
},
|
|
375
|
+
get manager() {
|
|
376
|
+
return manager;
|
|
377
|
+
},
|
|
378
|
+
execute(sql, params) {
|
|
379
|
+
return resolve().execute(sql, params);
|
|
380
|
+
},
|
|
381
|
+
get transaction() {
|
|
382
|
+
return resolve().transaction?.bind(resolve());
|
|
383
|
+
},
|
|
384
|
+
runInTransaction(batch) {
|
|
385
|
+
return resolve().runInTransaction(batch);
|
|
386
|
+
},
|
|
387
|
+
get dialect() {
|
|
388
|
+
return resolve().dialect;
|
|
389
|
+
},
|
|
390
|
+
ping() {
|
|
391
|
+
return resolve().ping();
|
|
392
|
+
},
|
|
393
|
+
close() {
|
|
394
|
+
return resolve().close();
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const db: DbService = createDbService(() => {
|
|
400
|
+
if (!instance) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
"[atlas] db singleton accessed before AtlasProvider.boot() ran. " +
|
|
403
|
+
"Check that `@c9up/atlas/provider` is listed in your reamrc.ts " +
|
|
404
|
+
"providers and that `config/database.ts` defines at least one " +
|
|
405
|
+
"connection.",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return instance;
|
|
97
409
|
});
|
|
98
410
|
|
|
99
411
|
export default db;
|