@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/BaseModel.ts
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AsyncDatabaseConnection,
|
|
3
|
+
TransactionOptions,
|
|
4
|
+
} from "./adapters/NapiDbAdapter.js";
|
|
2
5
|
import { BaseEntity } from "./BaseEntity.js";
|
|
3
6
|
import { BaseRepository, type DatabaseConnection } from "./BaseRepository.js";
|
|
4
7
|
import { ensureEntityMetadata } from "./decorators/entity.js";
|
|
5
8
|
import { AtlasError } from "./errors.js";
|
|
6
9
|
import { getConnection, getDb } from "./services/db.js";
|
|
10
|
+
import type { TransactionClient } from "./Transaction.js";
|
|
7
11
|
import { isTransactionClient } from "./utils/transactionBrand.js";
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Options accepted by the static finders/creators — Adonis Lucid's
|
|
15
|
+
* `{ client: trx }`. Routes the operation through a transaction client instead
|
|
16
|
+
* of the model's own connection.
|
|
17
|
+
*/
|
|
18
|
+
export interface ModelClientOptions {
|
|
19
|
+
client?: DatabaseConnection;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Narrow the optional trailing argument of an overloaded finder (which is a
|
|
24
|
+
* value in the `(column, value)` form, or the options in the `(clause)` form) to
|
|
25
|
+
* client options — without a cast. An options object without a `client` reads as
|
|
26
|
+
* "no options", which is the same as passing none.
|
|
27
|
+
*/
|
|
28
|
+
function isClientOptions(x: unknown): x is ModelClientOptions {
|
|
29
|
+
return typeof x === "object" && x !== null && "client" in x;
|
|
30
|
+
}
|
|
31
|
+
|
|
9
32
|
/** A concrete BaseModel subclass: a `new()` constructor plus the static façade. */
|
|
10
33
|
type ModelClass<T extends BaseModel> = (new () => T) & typeof BaseModel;
|
|
11
34
|
|
|
@@ -61,10 +84,58 @@ export abstract class BaseModel extends BaseEntity {
|
|
|
61
84
|
return conn;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The {@link BaseRepository} backing this model on its resolved connection.
|
|
89
|
+
* Pass `{ client: trx }` (Adonis Lucid) to bind the repository to a
|
|
90
|
+
* transaction, so every finder/creator routed through it runs on that trx.
|
|
91
|
+
*/
|
|
92
|
+
static $repo<T extends BaseModel>(
|
|
93
|
+
this: ModelClass<T>,
|
|
94
|
+
options?: ModelClientOptions,
|
|
95
|
+
): BaseRepository<T> {
|
|
66
96
|
this.$boot();
|
|
67
|
-
|
|
97
|
+
const repo = new BaseRepository<T>(this, this.$connection());
|
|
98
|
+
return options?.client ? repo.useTransaction(options.client) : repo;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Start a transaction on THIS model's connection (Adonis Lucid
|
|
103
|
+
* `Model.transaction`) — handy when the model overrides `static connection`.
|
|
104
|
+
* Managed when given a callback (auto commit on success, rollback on throw),
|
|
105
|
+
* manual otherwise. On the default connection it is equivalent to
|
|
106
|
+
* `db.transaction(...)`. Accepts an `isolationLevel` via the options.
|
|
107
|
+
*/
|
|
108
|
+
static transaction<T>(
|
|
109
|
+
this: ModelClass<BaseModel>,
|
|
110
|
+
callback: (trx: TransactionClient) => Promise<T> | T,
|
|
111
|
+
options?: TransactionOptions,
|
|
112
|
+
): Promise<T>;
|
|
113
|
+
static transaction(
|
|
114
|
+
this: ModelClass<BaseModel>,
|
|
115
|
+
options?: TransactionOptions,
|
|
116
|
+
): Promise<TransactionClient>;
|
|
117
|
+
static transaction(
|
|
118
|
+
this: ModelClass<BaseModel>,
|
|
119
|
+
callbackOrOptions?:
|
|
120
|
+
| ((trx: TransactionClient) => unknown)
|
|
121
|
+
| TransactionOptions,
|
|
122
|
+
options?: TransactionOptions,
|
|
123
|
+
): Promise<unknown> {
|
|
124
|
+
const conn = this.$connection();
|
|
125
|
+
if (typeof conn.transaction !== "function") {
|
|
126
|
+
throw new AtlasError(
|
|
127
|
+
"E_NO_INTERACTIVE_TRANSACTION",
|
|
128
|
+
`Model '${this.name}' connection has no interactive transaction().`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (typeof callbackOrOptions === "function") {
|
|
132
|
+
return conn.transaction(callbackOrOptions, options);
|
|
133
|
+
}
|
|
134
|
+
// Manual mode: forward the isolation options if any (the overload rejects an
|
|
135
|
+
// explicit `undefined`, so call with no args when none were passed).
|
|
136
|
+
return callbackOrOptions === undefined
|
|
137
|
+
? conn.transaction()
|
|
138
|
+
: conn.transaction(callbackOrOptions);
|
|
68
139
|
}
|
|
69
140
|
|
|
70
141
|
// — Static finders (AdonisJS Lucid) —
|
|
@@ -72,97 +143,126 @@ export abstract class BaseModel extends BaseEntity {
|
|
|
72
143
|
static find<T extends BaseModel>(
|
|
73
144
|
this: ModelClass<T>,
|
|
74
145
|
id: string | number,
|
|
146
|
+
options?: ModelClientOptions,
|
|
75
147
|
): Promise<T | null> {
|
|
76
|
-
return this.$repo().find(id);
|
|
148
|
+
return this.$repo(options).find(id);
|
|
77
149
|
}
|
|
78
150
|
|
|
79
151
|
static findOrFail<T extends BaseModel>(
|
|
80
152
|
this: ModelClass<T>,
|
|
81
153
|
id: string | number,
|
|
154
|
+
options?: ModelClientOptions,
|
|
82
155
|
): Promise<T> {
|
|
83
|
-
return this.$repo().findOrFail(id);
|
|
156
|
+
return this.$repo(options).findOrFail(id);
|
|
84
157
|
}
|
|
85
158
|
|
|
86
159
|
static findBy<T extends BaseModel>(
|
|
87
160
|
this: ModelClass<T>,
|
|
88
161
|
column: string,
|
|
89
162
|
value: unknown,
|
|
163
|
+
options?: ModelClientOptions,
|
|
90
164
|
): Promise<T | null>;
|
|
91
165
|
static findBy<T extends BaseModel>(
|
|
92
166
|
this: ModelClass<T>,
|
|
93
167
|
clause: Record<string, unknown>,
|
|
168
|
+
options?: ModelClientOptions,
|
|
94
169
|
): Promise<T | null>;
|
|
95
170
|
static findBy<T extends BaseModel>(
|
|
96
171
|
this: ModelClass<T>,
|
|
97
172
|
columnOrClause: string | Record<string, unknown>,
|
|
98
|
-
|
|
173
|
+
valueOrOptions?: unknown,
|
|
174
|
+
options?: ModelClientOptions,
|
|
99
175
|
): Promise<T | null> {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
176
|
+
if (typeof columnOrClause === "string") {
|
|
177
|
+
return this.$repo(options).findBy(columnOrClause, valueOrOptions);
|
|
178
|
+
}
|
|
179
|
+
const opts = isClientOptions(valueOrOptions) ? valueOrOptions : undefined;
|
|
180
|
+
return this.$repo(opts).findBy(columnOrClause);
|
|
103
181
|
}
|
|
104
182
|
|
|
105
183
|
static findByOrFail<T extends BaseModel>(
|
|
106
184
|
this: ModelClass<T>,
|
|
107
185
|
column: string,
|
|
108
186
|
value: unknown,
|
|
187
|
+
options?: ModelClientOptions,
|
|
109
188
|
): Promise<T>;
|
|
110
189
|
static findByOrFail<T extends BaseModel>(
|
|
111
190
|
this: ModelClass<T>,
|
|
112
191
|
clause: Record<string, unknown>,
|
|
192
|
+
options?: ModelClientOptions,
|
|
113
193
|
): Promise<T>;
|
|
114
194
|
static findByOrFail<T extends BaseModel>(
|
|
115
195
|
this: ModelClass<T>,
|
|
116
196
|
columnOrClause: string | Record<string, unknown>,
|
|
117
|
-
|
|
197
|
+
valueOrOptions?: unknown,
|
|
198
|
+
options?: ModelClientOptions,
|
|
118
199
|
): Promise<T> {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
200
|
+
if (typeof columnOrClause === "string") {
|
|
201
|
+
return this.$repo(options).findByOrFail(columnOrClause, valueOrOptions);
|
|
202
|
+
}
|
|
203
|
+
const opts = isClientOptions(valueOrOptions) ? valueOrOptions : undefined;
|
|
204
|
+
return this.$repo(opts).findByOrFail(columnOrClause);
|
|
122
205
|
}
|
|
123
206
|
|
|
124
207
|
static findMany<T extends BaseModel>(
|
|
125
208
|
this: ModelClass<T>,
|
|
126
209
|
ids: Array<string | number>,
|
|
210
|
+
options?: ModelClientOptions,
|
|
127
211
|
): Promise<T[]> {
|
|
128
|
-
return this.$repo().findMany(ids);
|
|
212
|
+
return this.$repo(options).findMany(ids);
|
|
129
213
|
}
|
|
130
214
|
|
|
131
215
|
static findManyBy<T extends BaseModel>(
|
|
132
216
|
this: ModelClass<T>,
|
|
133
217
|
column: string,
|
|
134
218
|
values: Array<string | number>,
|
|
219
|
+
options?: ModelClientOptions,
|
|
135
220
|
): Promise<T[]>;
|
|
136
221
|
static findManyBy<T extends BaseModel>(
|
|
137
222
|
this: ModelClass<T>,
|
|
138
223
|
clause: Record<string, unknown>,
|
|
224
|
+
options?: ModelClientOptions,
|
|
139
225
|
): Promise<T[]>;
|
|
140
226
|
static findManyBy<T extends BaseModel>(
|
|
141
227
|
this: ModelClass<T>,
|
|
142
228
|
columnOrClause: string | Record<string, unknown>,
|
|
143
|
-
|
|
229
|
+
valuesOrOptions?: Array<string | number> | ModelClientOptions,
|
|
230
|
+
options?: ModelClientOptions,
|
|
144
231
|
): Promise<T[]> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
232
|
+
if (typeof columnOrClause === "string") {
|
|
233
|
+
const values = Array.isArray(valuesOrOptions) ? valuesOrOptions : [];
|
|
234
|
+
return this.$repo(options).findManyBy(columnOrClause, values);
|
|
235
|
+
}
|
|
236
|
+
const opts = isClientOptions(valuesOrOptions) ? valuesOrOptions : undefined;
|
|
237
|
+
return this.$repo(opts).findManyBy(columnOrClause);
|
|
148
238
|
}
|
|
149
239
|
|
|
150
|
-
static all<T extends BaseModel>(
|
|
151
|
-
|
|
240
|
+
static all<T extends BaseModel>(
|
|
241
|
+
this: ModelClass<T>,
|
|
242
|
+
options?: ModelClientOptions,
|
|
243
|
+
): Promise<T[]> {
|
|
244
|
+
return this.$repo(options).all();
|
|
152
245
|
}
|
|
153
246
|
|
|
154
247
|
static query<T extends BaseModel>(
|
|
155
248
|
this: ModelClass<T>,
|
|
249
|
+
options?: ModelClientOptions,
|
|
156
250
|
): ReturnType<BaseRepository<T>["query"]> {
|
|
157
|
-
return this.$repo().query();
|
|
251
|
+
return this.$repo(options).query();
|
|
158
252
|
}
|
|
159
253
|
|
|
160
|
-
static first<T extends BaseModel>(
|
|
161
|
-
|
|
254
|
+
static first<T extends BaseModel>(
|
|
255
|
+
this: ModelClass<T>,
|
|
256
|
+
options?: ModelClientOptions,
|
|
257
|
+
): Promise<T | null> {
|
|
258
|
+
return this.$repo(options).query().first();
|
|
162
259
|
}
|
|
163
260
|
|
|
164
|
-
static firstOrFail<T extends BaseModel>(
|
|
165
|
-
|
|
261
|
+
static firstOrFail<T extends BaseModel>(
|
|
262
|
+
this: ModelClass<T>,
|
|
263
|
+
options?: ModelClientOptions,
|
|
264
|
+
): Promise<T> {
|
|
265
|
+
return this.$repo(options).query().firstOrFail();
|
|
166
266
|
}
|
|
167
267
|
|
|
168
268
|
// — Static creators (AdonisJS Lucid) —
|
|
@@ -170,93 +270,108 @@ export abstract class BaseModel extends BaseEntity {
|
|
|
170
270
|
static create<T extends BaseModel>(
|
|
171
271
|
this: ModelClass<T>,
|
|
172
272
|
data: Partial<Record<string, unknown>>,
|
|
273
|
+
options?: ModelClientOptions & { allowExtraProperties?: boolean },
|
|
173
274
|
): Promise<T> {
|
|
174
|
-
return this.$repo().create(data
|
|
275
|
+
return this.$repo(options).create(data, {
|
|
276
|
+
allowExtraProperties: options?.allowExtraProperties ?? false,
|
|
277
|
+
});
|
|
175
278
|
}
|
|
176
279
|
|
|
177
280
|
static createMany<T extends BaseModel>(
|
|
178
281
|
this: ModelClass<T>,
|
|
179
282
|
rows: Array<Partial<Record<string, unknown>>>,
|
|
283
|
+
options?: ModelClientOptions,
|
|
180
284
|
): Promise<T[]> {
|
|
181
|
-
return this.$repo().createMany(rows);
|
|
285
|
+
return this.$repo(options).createMany(rows);
|
|
182
286
|
}
|
|
183
287
|
|
|
184
288
|
/** {@link create} without firing lifecycle hooks (AdonisJS Lucid `createQuietly`). */
|
|
185
289
|
static createQuietly<T extends BaseModel>(
|
|
186
290
|
this: ModelClass<T>,
|
|
187
291
|
data: Partial<Record<string, unknown>>,
|
|
292
|
+
options?: ModelClientOptions,
|
|
188
293
|
): Promise<T> {
|
|
189
|
-
return this.$repo().createQuietly(data);
|
|
294
|
+
return this.$repo(options).createQuietly(data);
|
|
190
295
|
}
|
|
191
296
|
|
|
192
297
|
/** {@link createMany} without firing lifecycle hooks (AdonisJS Lucid `createManyQuietly`). */
|
|
193
298
|
static createManyQuietly<T extends BaseModel>(
|
|
194
299
|
this: ModelClass<T>,
|
|
195
300
|
rows: Array<Partial<Record<string, unknown>>>,
|
|
301
|
+
options?: ModelClientOptions,
|
|
196
302
|
): Promise<T[]> {
|
|
197
|
-
return this.$repo().createManyQuietly(rows);
|
|
303
|
+
return this.$repo(options).createManyQuietly(rows);
|
|
198
304
|
}
|
|
199
305
|
|
|
200
306
|
static firstOrCreate<T extends BaseModel>(
|
|
201
307
|
this: ModelClass<T>,
|
|
202
308
|
search: Record<string, unknown>,
|
|
203
309
|
values?: Record<string, unknown>,
|
|
310
|
+
options?: ModelClientOptions,
|
|
204
311
|
): Promise<T> {
|
|
205
|
-
return this.$repo().firstOrCreate(search, values);
|
|
312
|
+
return this.$repo(options).firstOrCreate(search, values);
|
|
206
313
|
}
|
|
207
314
|
|
|
208
315
|
static firstOrNew<T extends BaseModel>(
|
|
209
316
|
this: ModelClass<T>,
|
|
210
317
|
search: Record<string, unknown>,
|
|
211
318
|
values?: Record<string, unknown>,
|
|
319
|
+
options?: ModelClientOptions,
|
|
212
320
|
): Promise<T> {
|
|
213
|
-
return this.$repo().firstOrNew(search, values);
|
|
321
|
+
return this.$repo(options).firstOrNew(search, values);
|
|
214
322
|
}
|
|
215
323
|
|
|
216
324
|
static updateOrCreate<T extends BaseModel>(
|
|
217
325
|
this: ModelClass<T>,
|
|
218
326
|
search: Record<string, unknown>,
|
|
219
327
|
values: Record<string, unknown>,
|
|
328
|
+
options?: ModelClientOptions,
|
|
220
329
|
): Promise<T> {
|
|
221
|
-
return this.$repo().updateOrCreate(search, values);
|
|
330
|
+
return this.$repo(options).updateOrCreate(search, values);
|
|
222
331
|
}
|
|
223
332
|
|
|
224
333
|
static updateOrCreateMany<T extends BaseModel>(
|
|
225
334
|
this: ModelClass<T>,
|
|
226
335
|
key: string | string[],
|
|
227
336
|
rows: Array<Record<string, unknown>>,
|
|
337
|
+
options?: ModelClientOptions,
|
|
228
338
|
): Promise<T[]> {
|
|
229
|
-
return this.$repo().updateOrCreateMany(key, rows);
|
|
339
|
+
return this.$repo(options).updateOrCreateMany(key, rows);
|
|
230
340
|
}
|
|
231
341
|
|
|
232
342
|
static fetchOrCreateMany<T extends BaseModel>(
|
|
233
343
|
this: ModelClass<T>,
|
|
234
344
|
key: string | string[],
|
|
235
345
|
rows: Array<Record<string, unknown>>,
|
|
346
|
+
options?: ModelClientOptions,
|
|
236
347
|
): Promise<T[]> {
|
|
237
|
-
return this.$repo().fetchOrCreateMany(key, rows);
|
|
348
|
+
return this.$repo(options).fetchOrCreateMany(key, rows);
|
|
238
349
|
}
|
|
239
350
|
|
|
240
351
|
static fetchOrNewUpMany<T extends BaseModel>(
|
|
241
352
|
this: ModelClass<T>,
|
|
242
353
|
key: string | string[],
|
|
243
354
|
rows: Array<Record<string, unknown>>,
|
|
355
|
+
options?: ModelClientOptions,
|
|
244
356
|
): Promise<T[]> {
|
|
245
|
-
return this.$repo().fetchOrNewUpMany(key, rows);
|
|
357
|
+
return this.$repo(options).fetchOrNewUpMany(key, rows);
|
|
246
358
|
}
|
|
247
359
|
|
|
248
360
|
/** Empty this model's table (AdonisJS `Model.truncate`). `cascade` is Postgres-only. */
|
|
249
361
|
static truncate<T extends BaseModel>(
|
|
250
362
|
this: ModelClass<T>,
|
|
251
363
|
cascade = false,
|
|
364
|
+
options?: ModelClientOptions,
|
|
252
365
|
): Promise<void> {
|
|
253
|
-
return this.$repo().truncate(cascade);
|
|
366
|
+
return this.$repo(options).truncate(cascade);
|
|
254
367
|
}
|
|
255
368
|
|
|
256
369
|
// — Instance persistence (AdonisJS Lucid) —
|
|
257
370
|
|
|
258
371
|
/** Transaction bound to this instance via {@link useTransaction}, if any. */
|
|
259
372
|
#trx?: DatabaseConnection;
|
|
373
|
+
/** Named connection bound at runtime via {@link useConnection}, if any. */
|
|
374
|
+
#connectionOverride?: string;
|
|
260
375
|
|
|
261
376
|
/** The transaction bound to this instance, if any (AdonisJS Lucid `$trx`). */
|
|
262
377
|
get $trx(): DatabaseConnection | undefined {
|
|
@@ -294,8 +409,30 @@ export abstract class BaseModel extends BaseEntity {
|
|
|
294
409
|
* BaseModel subclass at runtime; TS types it only as `Function`, hence the
|
|
295
410
|
* single unavoidable narrowing (the pattern Lucid's own BaseModel uses).
|
|
296
411
|
*/
|
|
412
|
+
/**
|
|
413
|
+
* Bind this instance to a named connection at runtime (AdonisJS Lucid
|
|
414
|
+
* `model.useConnection`) — subsequent `save()`/`delete()`/relations run on it.
|
|
415
|
+
* A per-instance override of the class's `static connection`. Chainable.
|
|
416
|
+
*/
|
|
417
|
+
useConnection(name: string): this {
|
|
418
|
+
this.#connectionOverride = name;
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
|
|
297
422
|
#repo(): BaseRepository<this> {
|
|
298
423
|
const model = this.constructor as ModelClass<this>;
|
|
424
|
+
if (this.#connectionOverride !== undefined) {
|
|
425
|
+
const conn = getConnection(this.#connectionOverride);
|
|
426
|
+
if (!conn) {
|
|
427
|
+
throw new AtlasError(
|
|
428
|
+
"MISSING_CONNECTION",
|
|
429
|
+
`Model '${model.name}': no connection named '${this.#connectionOverride}' is registered.`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
model.$boot();
|
|
433
|
+
const repo = new BaseRepository<this>(model, conn);
|
|
434
|
+
return this.#trx ? repo.useTransaction(this.#trx) : repo;
|
|
435
|
+
}
|
|
299
436
|
const repo = model.$repo();
|
|
300
437
|
return this.#trx ? repo.useTransaction(this.#trx) : repo;
|
|
301
438
|
}
|
package/src/BaseRepository.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { fireHooks } from "./decorators/hooks.js";
|
|
37
37
|
import { AtlasError, EntityNotFoundError } from "./errors.js";
|
|
38
38
|
import { isAtlasStrictMode, ModelQuery } from "./ModelQuery.js";
|
|
39
|
+
import { DatabaseQueryBuilder } from "./query/DatabaseQueryBuilder.js";
|
|
39
40
|
import {
|
|
40
41
|
type AtlasDialect,
|
|
41
42
|
compileStatementNative,
|
|
@@ -122,6 +123,14 @@ function isUniqueKeyViolation(err: unknown): boolean {
|
|
|
122
123
|
* Drivers backed by `AsyncDatabaseConnection` (`createNapiConnection`)
|
|
123
124
|
* satisfy this interface out-of-the-box.
|
|
124
125
|
*/
|
|
126
|
+
/** Options for {@link BaseRepository.create} (Adonis Lucid `create` options). */
|
|
127
|
+
export interface CreateOptions {
|
|
128
|
+
/** Skip lifecycle hooks + event dispatch (atlas's legacy `quiet` flag). */
|
|
129
|
+
quiet?: boolean;
|
|
130
|
+
/** Silently drop keys that are not columns instead of throwing (Lucid default: throw). */
|
|
131
|
+
allowExtraProperties?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
export interface DatabaseConnection {
|
|
126
135
|
/**
|
|
127
136
|
* Run a write statement; returns rowsAffected.
|
|
@@ -232,6 +241,18 @@ export function computeCastTypes(
|
|
|
232
241
|
return out;
|
|
233
242
|
}
|
|
234
243
|
|
|
244
|
+
/** A model instance that can bind to a transaction (BaseModel `useTransaction`). */
|
|
245
|
+
interface TransactionBindable {
|
|
246
|
+
useTransaction(trx: DatabaseConnection): unknown;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Structural guard: does this entity expose `useTransaction` (i.e. is a BaseModel)? */
|
|
250
|
+
function isTransactionBindable(entity: object): entity is TransactionBindable {
|
|
251
|
+
return (
|
|
252
|
+
"useTransaction" in entity && typeof entity.useTransaction === "function"
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
235
256
|
export class BaseRepository<T extends BaseEntity> {
|
|
236
257
|
#entityClass: EntityConstructor<T>;
|
|
237
258
|
#tableName: string;
|
|
@@ -636,11 +657,17 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
636
657
|
* Build an entity from a plain object and persist it. Fires `beforeCreate` →
|
|
637
658
|
* `beforeSave` → INSERT → `afterCreate` → `afterSave` (AdonisJS/Lucid order:
|
|
638
659
|
* the specific hook runs before the general `beforeSave`).
|
|
660
|
+
*
|
|
661
|
+
* An unknown key throws by default (Lucid); pass `{ allowExtraProperties: true }`
|
|
662
|
+
* to drop unknown keys instead. A bare boolean is the legacy `quiet` flag.
|
|
639
663
|
*/
|
|
640
664
|
async create(
|
|
641
665
|
data: Partial<Record<string, unknown>>,
|
|
642
|
-
|
|
666
|
+
options: boolean | CreateOptions = false,
|
|
643
667
|
): Promise<T> {
|
|
668
|
+
// Back-compat: a bare boolean is the legacy `quiet` flag.
|
|
669
|
+
const { quiet = false, allowExtraProperties = false } =
|
|
670
|
+
typeof options === "boolean" ? { quiet: options } : options;
|
|
644
671
|
const entity = new this.#entityClass();
|
|
645
672
|
for (const [key, value] of Object.entries(data)) {
|
|
646
673
|
if (
|
|
@@ -650,6 +677,18 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
650
677
|
const prop = this.#toProperty(key);
|
|
651
678
|
entity.assertMassAssignable(prop);
|
|
652
679
|
entity.setProp(prop, value);
|
|
680
|
+
} else if (key.startsWith("$")) {
|
|
681
|
+
// Framework-internal (`$extras`, `$trx`, …) — leaks in when an entity
|
|
682
|
+
// instance is passed as data. Not a user column and not a typo; skip.
|
|
683
|
+
} else if (!allowExtraProperties) {
|
|
684
|
+
// Adonis Lucid throws on an unknown property by default (a typo'd or
|
|
685
|
+
// stray key is a bug, not something to silently drop). Opt out with
|
|
686
|
+
// `create(data, { allowExtraProperties: true })`.
|
|
687
|
+
throw new AtlasError(
|
|
688
|
+
"E_UNKNOWN_COLUMN",
|
|
689
|
+
`Cannot assign '${key}' — it is not a column on ${this.#entityClass.name}. ` +
|
|
690
|
+
"Pass { allowExtraProperties: true } to drop unknown keys instead.",
|
|
691
|
+
);
|
|
653
692
|
}
|
|
654
693
|
}
|
|
655
694
|
if (!quiet) {
|
|
@@ -1366,18 +1405,52 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
1366
1405
|
return result;
|
|
1367
1406
|
}
|
|
1368
1407
|
// No explicit `@Column({ prepare })`: lower a `@column.date()` /
|
|
1369
|
-
// `@column.dateTime()` value to its ISO 8601 string for the SQL bind.
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
// being passed raw to the N-API bind.
|
|
1408
|
+
// `@column.dateTime()` value to its ISO 8601 string for the SQL bind.
|
|
1409
|
+
// Branch order mirrors Lucid's `prepareDateColumn` (strings pass through,
|
|
1410
|
+
// `DateTime` is formatted, anything else throws naming the column) — see
|
|
1411
|
+
// `#prepareDateString` for the one named deviation.
|
|
1374
1412
|
if (this.#dateColumns[propertyKey] && value != null) {
|
|
1413
|
+
if (typeof value === "string") {
|
|
1414
|
+
return this.#prepareDateString(propertyKey, value);
|
|
1415
|
+
}
|
|
1416
|
+
// A raw JS `Date` is accepted where Lucid throws: `toISOString()` is
|
|
1417
|
+
// unambiguous UTC, so the strictness would buy nothing. Named deviation.
|
|
1375
1418
|
if (value instanceof Date) return value.toISOString();
|
|
1419
|
+
// Otherwise the Chronos adapter's prepare serialises a `DateTime` — via
|
|
1420
|
+
// a STRUCTURAL check, so an instance from a duplicated `@c9up/chronos`
|
|
1421
|
+
// copy (another realm) round-trips instead of being passed raw to the
|
|
1422
|
+
// N-API bind.
|
|
1376
1423
|
return dateTimeAtlasAdapter.prepare(value);
|
|
1377
1424
|
}
|
|
1378
1425
|
return value;
|
|
1379
1426
|
}
|
|
1380
1427
|
|
|
1428
|
+
/**
|
|
1429
|
+
* Lower a string assigned to a `@column.date()` / `@column.dateTime()`.
|
|
1430
|
+
*
|
|
1431
|
+
* Lucid lets every string through untouched (`prepareDateColumn`, first
|
|
1432
|
+
* branch), and atlas does the same — with ONE named deviation: a *naive*
|
|
1433
|
+
* datetime (no `Z`, no offset) is rejected. Such a string has no instant
|
|
1434
|
+
* attached, so the chronos read path resolves it in the JS runtime's local
|
|
1435
|
+
* zone: the very same row then hydrates to a different instant on a
|
|
1436
|
+
* developer laptop in Europe/Zurich and on a CI host running UTC. Silently
|
|
1437
|
+
* storing a value that means two different things is the failure this guard
|
|
1438
|
+
* exists to prevent; date-only strings and offset-bearing ones are
|
|
1439
|
+
* unambiguous and pass exactly like Lucid.
|
|
1440
|
+
*/
|
|
1441
|
+
#prepareDateString(propertyKey: string, value: string): string {
|
|
1442
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value.trim());
|
|
1443
|
+
const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/.test(value.trim());
|
|
1444
|
+
if (dateOnly || hasZone) return value;
|
|
1445
|
+
throw new AtlasError(
|
|
1446
|
+
"INVALID_DATE_COLUMN_VALUE",
|
|
1447
|
+
`${this.#entityClass.name}.${propertyKey}: "${value}" is a naive datetime — it carries no timezone, so it would read back differently depending on the machine.`,
|
|
1448
|
+
{
|
|
1449
|
+
hint: 'Add an offset ("2026-08-10T12:00:00Z"), pass a chronos DateTime, or use a date-only string ("2026-08-10").',
|
|
1450
|
+
},
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1381
1454
|
#applyConsume(propertyKey: string, value: unknown, model?: unknown): unknown {
|
|
1382
1455
|
const consume = this.#columnConsumes.get(propertyKey);
|
|
1383
1456
|
if (consume) {
|
|
@@ -1892,6 +1965,17 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
1892
1965
|
enumerable: false,
|
|
1893
1966
|
configurable: true,
|
|
1894
1967
|
});
|
|
1968
|
+
// Adonis Lucid `$trx` propagation: an instance loaded or created THROUGH a
|
|
1969
|
+
// transaction-bound repo binds to that transaction, so its own
|
|
1970
|
+
// `save()`/`delete()` stay inside it (and reset when the trx settles, via
|
|
1971
|
+
// the release hook `useTransaction` registers). Previously only an explicit
|
|
1972
|
+
// `model.useTransaction(trx)` did this, so a row read with
|
|
1973
|
+
// `Model.query({ client: trx })` could later save OUTSIDE the transaction.
|
|
1974
|
+
// Only BaseModel exposes `useTransaction`; a plain BaseEntity persists via
|
|
1975
|
+
// the repo directly, so it needs no instance binding.
|
|
1976
|
+
if (this.#durableParent !== undefined && isTransactionBindable(entity)) {
|
|
1977
|
+
entity.useTransaction(this.#db);
|
|
1978
|
+
}
|
|
1895
1979
|
}
|
|
1896
1980
|
|
|
1897
1981
|
/**
|
|
@@ -2245,7 +2329,10 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
2245
2329
|
// Resolve the related PK to its DB column (multi-word / columnName),
|
|
2246
2330
|
// mirroring the eager-preload fix — a raw property name here targets
|
|
2247
2331
|
// the wrong column in the correlated EXISTS.
|
|
2248
|
-
|
|
2332
|
+
// The related-side key the pivot's otherKey references — the related
|
|
2333
|
+
// PK unless `relatedKey` overrides it (Adonis Lucid `relatedKey`).
|
|
2334
|
+
const relatedPkProp =
|
|
2335
|
+
pivot.relatedKey ?? getPrimaryKey(relatedClass) ?? "id";
|
|
2249
2336
|
const relatedPk =
|
|
2250
2337
|
getColumnMetadata(relatedClass).find(
|
|
2251
2338
|
(c) => c.propertyKey === relatedPkProp,
|
|
@@ -2498,7 +2585,7 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
2498
2585
|
// pivot FK, so the `::cast` must match that column's type.
|
|
2499
2586
|
const parentPkCast = this.#castTypes[this.#dbColumn(parentPk)];
|
|
2500
2587
|
if (parentPkCast) pivotKeyCasts[pivotFk] = parentPkCast;
|
|
2501
|
-
const relatedPk = getPrimaryKey(relatedClass) ?? "id";
|
|
2588
|
+
const relatedPk = pivot.relatedKey ?? getPrimaryKey(relatedClass) ?? "id";
|
|
2502
2589
|
const relatedPkDb =
|
|
2503
2590
|
getColumnMetadata(relatedClass).find((c) => c.propertyKey === relatedPk)
|
|
2504
2591
|
?.columnName ?? camelToSnake(relatedPk);
|
|
@@ -2848,47 +2935,69 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
2848
2935
|
// runs in ONE transaction via `withParentSaved` (AdonisJS/Lucid parity):
|
|
2849
2936
|
// atomic, rolled back on any failure (no orphan related row, no pivot to a
|
|
2850
2937
|
// missing parent), with domain events flushed only after commit.
|
|
2851
|
-
const relatedPkProp =
|
|
2938
|
+
const relatedPkProp =
|
|
2939
|
+
pivot.relatedKey ?? getPrimaryKey(relatedClass) ?? "id";
|
|
2852
2940
|
const attachRows = (
|
|
2853
2941
|
rows: BaseEntity[],
|
|
2854
2942
|
trx: TransactionClient,
|
|
2855
2943
|
fk: unknown,
|
|
2944
|
+
pivotFor?: (index: number) => Record<string, unknown>,
|
|
2856
2945
|
): Promise<void> => {
|
|
2857
2946
|
if (rows.length === 0) return Promise.resolve();
|
|
2858
2947
|
const arg: Record<string, Record<string, unknown>> = {};
|
|
2859
|
-
|
|
2948
|
+
rows.forEach((r, i) => {
|
|
2949
|
+
arg[String(r[relatedPkProp])] = pivotFor?.(i) ?? {};
|
|
2950
|
+
});
|
|
2860
2951
|
return attach(arg, trx, fk);
|
|
2861
2952
|
};
|
|
2953
|
+
// create/save accept per-row pivot attributes (Adonis Lucid `create(values,
|
|
2954
|
+
// pivotAttributes)` / `save(related, pivotAttributes)`) — written onto the
|
|
2955
|
+
// pivot row alongside the FK/otherKey, in the same transaction.
|
|
2862
2956
|
const m2mOps = {
|
|
2863
|
-
create: (
|
|
2957
|
+
create: (
|
|
2958
|
+
data: Record<string, unknown>,
|
|
2959
|
+
pivotAttributes?: Record<string, unknown>,
|
|
2960
|
+
): Promise<BaseEntity> =>
|
|
2864
2961
|
withParentSaved(async (fk, rel, trx) => {
|
|
2865
2962
|
const created = await rel.create(data);
|
|
2866
|
-
await attachRows([created], trx, fk);
|
|
2963
|
+
await attachRows([created], trx, fk, () => pivotAttributes ?? {});
|
|
2867
2964
|
trx.after("commit", () => flushEvents([created]));
|
|
2868
2965
|
return created;
|
|
2869
2966
|
}),
|
|
2870
2967
|
createMany: (
|
|
2871
2968
|
rows: Array<Record<string, unknown>>,
|
|
2969
|
+
pivotAttributes?: Array<Record<string, unknown>>,
|
|
2872
2970
|
): Promise<BaseEntity[]> =>
|
|
2873
2971
|
withParentSaved(async (fk, rel, trx) => {
|
|
2874
2972
|
const created = await rel.createMany(rows);
|
|
2875
|
-
await attachRows(
|
|
2973
|
+
await attachRows(
|
|
2974
|
+
created,
|
|
2975
|
+
trx,
|
|
2976
|
+
fk,
|
|
2977
|
+
(i) => pivotAttributes?.[i] ?? {},
|
|
2978
|
+
);
|
|
2876
2979
|
// NO wrapper flush: rel.createMany now self-dispatches via
|
|
2877
2980
|
// #inManagedTx (like saveMany). The pivot rows carry no events; a
|
|
2878
2981
|
// second flush would double the related rows' events on a partial
|
|
2879
2982
|
// sink failure.
|
|
2880
2983
|
return created;
|
|
2881
2984
|
}),
|
|
2882
|
-
save: (
|
|
2985
|
+
save: (
|
|
2986
|
+
related: BaseEntity,
|
|
2987
|
+
pivotAttributes?: Record<string, unknown>,
|
|
2988
|
+
): Promise<void> =>
|
|
2883
2989
|
withParentSaved(async (fk, rel, trx) => {
|
|
2884
2990
|
await rel.save(related);
|
|
2885
|
-
await attachRows([related], trx, fk);
|
|
2991
|
+
await attachRows([related], trx, fk, () => pivotAttributes ?? {});
|
|
2886
2992
|
trx.after("commit", () => flushEvents([related]));
|
|
2887
2993
|
}),
|
|
2888
|
-
saveMany: (
|
|
2994
|
+
saveMany: (
|
|
2995
|
+
related: BaseEntity[],
|
|
2996
|
+
pivotAttributes?: Array<Record<string, unknown>>,
|
|
2997
|
+
): Promise<BaseEntity[]> =>
|
|
2889
2998
|
withParentSaved(async (fk, rel, trx) => {
|
|
2890
2999
|
const saved = await rel.saveMany(related);
|
|
2891
|
-
await attachRows(saved, trx, fk);
|
|
3000
|
+
await attachRows(saved, trx, fk, (i) => pivotAttributes?.[i] ?? {});
|
|
2892
3001
|
// NO wrapper flush: rel.saveMany self-dispatches via #inManagedTx
|
|
2893
3002
|
// (all-or-nothing). A second flush would double on partial failure.
|
|
2894
3003
|
return saved;
|
|
@@ -2914,6 +3023,16 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
2914
3023
|
type: "manyToMany",
|
|
2915
3024
|
...m2mOps,
|
|
2916
3025
|
query: scopedQuery,
|
|
3026
|
+
// A query builder on the PIVOT table itself, scoped to this parent
|
|
3027
|
+
// (Adonis Lucid `pivotQuery`) — for reading/updating/deleting pivot
|
|
3028
|
+
// rows directly, beyond attach/detach/sync.
|
|
3029
|
+
pivotQuery: () => {
|
|
3030
|
+
guardParent("pivotQuery()");
|
|
3031
|
+
return new DatabaseQueryBuilder(this.#db, dialect, pivotTable).where(
|
|
3032
|
+
pivotFk,
|
|
3033
|
+
readParentId(),
|
|
3034
|
+
);
|
|
3035
|
+
},
|
|
2917
3036
|
// async so the guard throw surfaces as a REJECTED promise — a method
|
|
2918
3037
|
// typed `Promise<void>` must never throw synchronously.
|
|
2919
3038
|
attach: async (ids) => {
|