@c9up/atlas 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +8 -0
  2. package/db.darwin-arm64.node +0 -0
  3. package/db.darwin-x64.node +0 -0
  4. package/db.linux-arm64-gnu.node +0 -0
  5. package/db.linux-x64-gnu.node +0 -0
  6. package/db.win32-x64-msvc.node +0 -0
  7. package/dist/AtlasProvider.d.ts +60 -7
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +204 -38
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +17 -0
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js.map +1 -1
  14. package/dist/BaseModel.d.ts +63 -27
  15. package/dist/BaseModel.d.ts.map +1 -1
  16. package/dist/BaseModel.js +108 -51
  17. package/dist/BaseModel.js.map +1 -1
  18. package/dist/BaseRepository.d.ts +11 -1
  19. package/dist/BaseRepository.d.ts.map +1 -1
  20. package/dist/BaseRepository.js +95 -20
  21. package/dist/BaseRepository.js.map +1 -1
  22. package/dist/ConnectionManager.d.ts +106 -0
  23. package/dist/ConnectionManager.d.ts.map +1 -0
  24. package/dist/ConnectionManager.js +228 -0
  25. package/dist/ConnectionManager.js.map +1 -0
  26. package/dist/ModelQuery.d.ts +256 -43
  27. package/dist/ModelQuery.d.ts.map +1 -1
  28. package/dist/ModelQuery.js +899 -231
  29. package/dist/ModelQuery.js.map +1 -1
  30. package/dist/Transaction.d.ts +54 -1
  31. package/dist/Transaction.d.ts.map +1 -1
  32. package/dist/Transaction.js +121 -59
  33. package/dist/Transaction.js.map +1 -1
  34. package/dist/adapters/NapiDbAdapter.d.ts +11 -0
  35. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  36. package/dist/adapters/NapiDbAdapter.js +35 -7
  37. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  38. package/dist/console/contract.d.ts +62 -0
  39. package/dist/console/contract.d.ts.map +1 -0
  40. package/dist/console/contract.js +38 -0
  41. package/dist/console/contract.js.map +1 -0
  42. package/dist/console/factoryCommands.d.ts +23 -0
  43. package/dist/console/factoryCommands.d.ts.map +1 -0
  44. package/dist/console/factoryCommands.js +62 -0
  45. package/dist/console/factoryCommands.js.map +1 -0
  46. package/dist/console/migrationCommands.d.ts +38 -8
  47. package/dist/console/migrationCommands.d.ts.map +1 -1
  48. package/dist/console/migrationCommands.js +157 -50
  49. package/dist/console/migrationCommands.js.map +1 -1
  50. package/dist/console/schemaCheckCommand.d.ts +11 -17
  51. package/dist/console/schemaCheckCommand.d.ts.map +1 -1
  52. package/dist/console/schemaCheckCommand.js +21 -15
  53. package/dist/console/schemaCheckCommand.js.map +1 -1
  54. package/dist/console/schemaDumpCommand.d.ts +30 -0
  55. package/dist/console/schemaDumpCommand.d.ts.map +1 -0
  56. package/dist/console/schemaDumpCommand.js +69 -0
  57. package/dist/console/schemaDumpCommand.js.map +1 -0
  58. package/dist/console/schemaGenerateCommand.d.ts +100 -0
  59. package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
  60. package/dist/console/schemaGenerateCommand.js +246 -0
  61. package/dist/console/schemaGenerateCommand.js.map +1 -0
  62. package/dist/console/seederCommands.d.ts +46 -0
  63. package/dist/console/seederCommands.d.ts.map +1 -0
  64. package/dist/console/seederCommands.js +136 -0
  65. package/dist/console/seederCommands.js.map +1 -0
  66. package/dist/decorators/entity.d.ts +13 -2
  67. package/dist/decorators/entity.d.ts.map +1 -1
  68. package/dist/decorators/entity.js +10 -0
  69. package/dist/decorators/entity.js.map +1 -1
  70. package/dist/decorators/hooks.d.ts +3 -3
  71. package/dist/decorators/hooks.d.ts.map +1 -1
  72. package/dist/decorators/hooks.js.map +1 -1
  73. package/dist/events.d.ts +5 -0
  74. package/dist/events.d.ts.map +1 -1
  75. package/dist/events.js.map +1 -1
  76. package/dist/factories.d.ts +8 -0
  77. package/dist/factories.d.ts.map +1 -0
  78. package/dist/factories.js +8 -0
  79. package/dist/factories.js.map +1 -0
  80. package/dist/index.d.ts +11 -5
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +12 -4
  83. package/dist/index.js.map +1 -1
  84. package/dist/lucid-schema.d.ts +15 -0
  85. package/dist/lucid-schema.d.ts.map +1 -0
  86. package/dist/lucid-schema.js +16 -0
  87. package/dist/lucid-schema.js.map +1 -0
  88. package/dist/orm.d.ts +14 -0
  89. package/dist/orm.d.ts.map +1 -0
  90. package/dist/orm.js +14 -0
  91. package/dist/orm.js.map +1 -0
  92. package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
  93. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
  94. package/dist/query/DatabaseQueryBuilder.js +1798 -0
  95. package/dist/query/DatabaseQueryBuilder.js.map +1 -0
  96. package/dist/query/DmlBuilder.d.ts +62 -0
  97. package/dist/query/DmlBuilder.d.ts.map +1 -0
  98. package/dist/query/DmlBuilder.js +84 -0
  99. package/dist/query/DmlBuilder.js.map +1 -0
  100. package/dist/query/QueryBuilder.d.ts +5 -0
  101. package/dist/query/QueryBuilder.d.ts.map +1 -1
  102. package/dist/query/QueryBuilder.js +7 -0
  103. package/dist/query/QueryBuilder.js.map +1 -1
  104. package/dist/query/RawQueryBuilder.d.ts +47 -0
  105. package/dist/query/RawQueryBuilder.d.ts.map +1 -0
  106. package/dist/query/RawQueryBuilder.js +138 -0
  107. package/dist/query/RawQueryBuilder.js.map +1 -0
  108. package/dist/query/interpolate.d.ts +45 -0
  109. package/dist/query/interpolate.d.ts.map +1 -0
  110. package/dist/query/interpolate.js +51 -0
  111. package/dist/query/interpolate.js.map +1 -0
  112. package/dist/query/native.d.ts +8 -0
  113. package/dist/query/native.d.ts.map +1 -1
  114. package/dist/query/native.js +14 -0
  115. package/dist/query/native.js.map +1 -1
  116. package/dist/query/operators.d.ts +4 -0
  117. package/dist/query/operators.d.ts.map +1 -0
  118. package/dist/query/operators.js +27 -0
  119. package/dist/query/operators.js.map +1 -0
  120. package/dist/schema/Migration.d.ts +31 -7
  121. package/dist/schema/Migration.d.ts.map +1 -1
  122. package/dist/schema/Migration.js +34 -7
  123. package/dist/schema/Migration.js.map +1 -1
  124. package/dist/schema/MigrationRunner.d.ts +56 -6
  125. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  126. package/dist/schema/MigrationRunner.js +382 -112
  127. package/dist/schema/MigrationRunner.js.map +1 -1
  128. package/dist/schema/Schema.d.ts +59 -2
  129. package/dist/schema/Schema.d.ts.map +1 -1
  130. package/dist/schema/Schema.js +143 -9
  131. package/dist/schema/Schema.js.map +1 -1
  132. package/dist/schema/SchemaDumper.d.ts +121 -0
  133. package/dist/schema/SchemaDumper.d.ts.map +1 -0
  134. package/dist/schema/SchemaDumper.js +365 -0
  135. package/dist/schema/SchemaDumper.js.map +1 -0
  136. package/dist/schema/Seeder.d.ts +13 -0
  137. package/dist/schema/Seeder.d.ts.map +1 -1
  138. package/dist/schema/Seeder.js +28 -5
  139. package/dist/schema/Seeder.js.map +1 -1
  140. package/dist/schema/TableBuilder.d.ts +27 -8
  141. package/dist/schema/TableBuilder.d.ts.map +1 -1
  142. package/dist/schema/TableBuilder.js +50 -12
  143. package/dist/schema/TableBuilder.js.map +1 -1
  144. package/dist/schema/catalog.d.ts +25 -1
  145. package/dist/schema/catalog.d.ts.map +1 -1
  146. package/dist/schema/catalog.js +69 -22
  147. package/dist/schema/catalog.js.map +1 -1
  148. package/dist/schema/introspect.d.ts +1 -1
  149. package/dist/schema/introspect.d.ts.map +1 -1
  150. package/dist/schema/introspect.js +26 -11
  151. package/dist/schema/introspect.js.map +1 -1
  152. package/dist/seeders.d.ts +7 -0
  153. package/dist/seeders.d.ts.map +1 -0
  154. package/dist/seeders.js +7 -0
  155. package/dist/seeders.js.map +1 -0
  156. package/dist/services/db.d.ts +120 -9
  157. package/dist/services/db.d.ts.map +1 -1
  158. package/dist/services/db.js +182 -26
  159. package/dist/services/db.js.map +1 -1
  160. package/dist/testing/DatabaseCleanup.d.ts +29 -6
  161. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  162. package/dist/testing/DatabaseCleanup.js +44 -17
  163. package/dist/testing/DatabaseCleanup.js.map +1 -1
  164. package/dist/testing/DbAssertions.d.ts +48 -0
  165. package/dist/testing/DbAssertions.d.ts.map +1 -0
  166. package/dist/testing/DbAssertions.js +80 -0
  167. package/dist/testing/DbAssertions.js.map +1 -0
  168. package/dist/testing/Factory.d.ts +111 -25
  169. package/dist/testing/Factory.d.ts.map +1 -1
  170. package/dist/testing/Factory.js +331 -56
  171. package/dist/testing/Factory.js.map +1 -1
  172. package/dist/testing/TestUtils.d.ts +49 -0
  173. package/dist/testing/TestUtils.d.ts.map +1 -0
  174. package/dist/testing/TestUtils.js +79 -0
  175. package/dist/testing/TestUtils.js.map +1 -0
  176. package/dist/testing/index.d.ts +3 -1
  177. package/dist/testing/index.d.ts.map +1 -1
  178. package/dist/testing/index.js +3 -1
  179. package/dist/testing/index.js.map +1 -1
  180. package/dist/types/relations.d.ts +31 -0
  181. package/dist/types/relations.d.ts.map +1 -0
  182. package/dist/types/relations.js +20 -0
  183. package/dist/types/relations.js.map +1 -0
  184. package/index.darwin-arm64.node +0 -0
  185. package/index.darwin-x64.node +0 -0
  186. package/index.linux-arm64-gnu.node +0 -0
  187. package/index.linux-x64-gnu.node +0 -0
  188. package/index.win32-x64-msvc.node +0 -0
  189. package/package.json +30 -2
  190. package/src/AtlasProvider.ts +297 -51
  191. package/src/BaseEntity.ts +29 -0
  192. package/src/BaseModel.ts +174 -37
  193. package/src/BaseRepository.ts +136 -17
  194. package/src/ConnectionManager.ts +298 -0
  195. package/src/ModelQuery.ts +1309 -260
  196. package/src/Transaction.ts +194 -60
  197. package/src/adapters/NapiDbAdapter.ts +72 -14
  198. package/src/console/contract.ts +96 -0
  199. package/src/console/factoryCommands.ts +73 -0
  200. package/src/console/migrationCommands.ts +253 -63
  201. package/src/console/schemaCheckCommand.ts +26 -22
  202. package/src/console/schemaDumpCommand.ts +96 -0
  203. package/src/console/schemaGenerateCommand.ts +380 -0
  204. package/src/console/seederCommands.ts +174 -0
  205. package/src/decorators/entity.ts +18 -2
  206. package/src/decorators/hooks.ts +5 -3
  207. package/src/events.ts +5 -0
  208. package/src/factories.ts +10 -0
  209. package/src/index.ts +50 -3
  210. package/src/lucid-schema.ts +22 -0
  211. package/src/orm.ts +13 -0
  212. package/src/query/DatabaseQueryBuilder.ts +2724 -0
  213. package/src/query/DmlBuilder.ts +131 -0
  214. package/src/query/QueryBuilder.ts +8 -0
  215. package/src/query/RawQueryBuilder.ts +173 -0
  216. package/src/query/interpolate.ts +73 -0
  217. package/src/query/native.ts +29 -0
  218. package/src/query/operators.ts +27 -0
  219. package/src/schema/Migration.ts +36 -7
  220. package/src/schema/MigrationRunner.ts +483 -119
  221. package/src/schema/Schema.ts +168 -19
  222. package/src/schema/SchemaDumper.ts +518 -0
  223. package/src/schema/Seeder.ts +47 -10
  224. package/src/schema/TableBuilder.ts +48 -11
  225. package/src/schema/catalog.ts +94 -24
  226. package/src/schema/introspect.ts +26 -9
  227. package/src/seeders.ts +16 -0
  228. package/src/services/db.ts +343 -31
  229. package/src/testing/DatabaseCleanup.ts +73 -21
  230. package/src/testing/DbAssertions.ts +127 -0
  231. package/src/testing/Factory.ts +559 -79
  232. package/src/testing/TestUtils.ts +123 -0
  233. package/src/testing/index.ts +12 -1
  234. package/src/types/relations.ts +43 -0
package/src/BaseModel.ts CHANGED
@@ -1,11 +1,34 @@
1
- import type { AsyncDatabaseConnection } from "./adapters/NapiDbAdapter.js";
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
- /** The {@link BaseRepository} backing this model on its resolved connection. */
65
- static $repo<T extends BaseModel>(this: ModelClass<T>): BaseRepository<T> {
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
- return new BaseRepository<T>(this, this.$connection());
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
- value?: unknown,
173
+ valueOrOptions?: unknown,
174
+ options?: ModelClientOptions,
99
175
  ): Promise<T | null> {
100
- return typeof columnOrClause === "string"
101
- ? this.$repo().findBy(columnOrClause, value)
102
- : this.$repo().findBy(columnOrClause);
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
- value?: unknown,
197
+ valueOrOptions?: unknown,
198
+ options?: ModelClientOptions,
118
199
  ): Promise<T> {
119
- return typeof columnOrClause === "string"
120
- ? this.$repo().findByOrFail(columnOrClause, value)
121
- : this.$repo().findByOrFail(columnOrClause);
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
- values?: Array<string | number>,
229
+ valuesOrOptions?: Array<string | number> | ModelClientOptions,
230
+ options?: ModelClientOptions,
144
231
  ): Promise<T[]> {
145
- return typeof columnOrClause === "string"
146
- ? this.$repo().findManyBy(columnOrClause, values ?? [])
147
- : this.$repo().findManyBy(columnOrClause);
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>(this: ModelClass<T>): Promise<T[]> {
151
- return this.$repo().all();
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>(this: ModelClass<T>): Promise<T | null> {
161
- return this.$repo().query().first();
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>(this: ModelClass<T>): Promise<T> {
165
- return this.$repo().query().firstOrFail();
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
  }
@@ -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
- quiet = false,
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. A raw
1370
- // JS `Date` is accepted leniently; otherwise the Chronos adapter's prepare
1371
- // serialises a `DateTime` via a STRUCTURAL check, so an instance from a
1372
- // duplicated `@c9up/chronos` copy (another realm) round-trips instead of
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
- const relatedPkProp = getPrimaryKey(relatedClass) ?? "id";
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 = getPrimaryKey(relatedClass) ?? "id";
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
- for (const r of rows) arg[String(r[relatedPkProp])] = {};
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: (data: Record<string, unknown>): Promise<BaseEntity> =>
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(created, trx, fk);
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: (related: BaseEntity): Promise<void> =>
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: (related: BaseEntity[]): Promise<BaseEntity[]> =>
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) => {