@c9up/atlas 0.2.0 → 0.2.2

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 +54 -0
  165. package/dist/testing/DbAssertions.d.ts.map +1 -0
  166. package/dist/testing/DbAssertions.js +156 -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 +220 -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
@@ -19,6 +19,7 @@ import type { BaseEntity } from "../BaseEntity.js";
19
19
  import { BaseRepository, type DatabaseConnection } from "../BaseRepository.js";
20
20
  import { getPrimaryKey, getRelationMetadata } from "../decorators/entity.js";
21
21
  import { getConnection } from "../services/db.js";
22
+ import { transaction } from "../Transaction.js";
22
23
 
23
24
  type EntityConstructor<T extends BaseEntity> = new () => T;
24
25
 
@@ -33,19 +34,51 @@ interface WithRequest {
33
34
  }
34
35
 
35
36
  /**
36
- * Context handed to the `define` callback (Adonis Lucid parity). Currently just
37
- * a Faker instance for generating fake data. A callback that takes no argument
38
- * keeps working it simply ignores the context.
37
+ * Per-factory internals reached across factory closures to support NESTED
38
+ * `.with()` (Adonis Lucid: `post.with('comments', 5)` inside a parent's `.with`
39
+ * callback). Kept off the public {@link FactoryBuilder} type and keyed by the
40
+ * builder object in a {@link WeakMap} — no `any`/cast, no leaked API surface.
41
+ */
42
+ interface FactoryInternals {
43
+ /** Take and clear the queued `.with()` requests (before a `make` clears them). */
44
+ consumeWith(): WithRequest[];
45
+ /** Take and clear the queued m2m `.pivotAttributes()` (set in a `.with` callback). */
46
+ consumePivot():
47
+ | Record<string, unknown>
48
+ | Array<Record<string, unknown>>
49
+ | undefined;
50
+ /** Persist this factory's `.with()` relations onto an already-created parent. */
51
+ applyRelations(
52
+ parent: BaseEntity,
53
+ reqs: WithRequest[],
54
+ db: DatabaseConnection,
55
+ recursive?: Record<string, unknown>,
56
+ ): Promise<void>;
57
+ }
58
+
59
+ const factoryInternals = new WeakMap<object, FactoryInternals>();
60
+
61
+ /**
62
+ * Runtime context handed to the factory callbacks (Adonis Lucid parity):
63
+ * `faker` for fake data, `isStubbed` (true during a `makeStubbed*` build), and
64
+ * `$trx` — the bound transaction/connection, if any, so a hook's own DB queries
65
+ * commit/roll back alongside the factory. A callback that ignores it keeps
66
+ * working.
39
67
  */
40
68
  export interface FactoryContext {
41
69
  faker: Faker;
70
+ isStubbed: boolean;
71
+ $trx?: DatabaseConnection;
42
72
  }
43
73
 
44
74
  /** Attributes callback — receives {@link FactoryContext}, returns the row shape. */
45
75
  type DefaultsFn = (ctx: FactoryContext) => Record<string, unknown>;
46
76
 
47
- /** A named state — mutates an in-progress data object in place. */
48
- type StateFn<D> = (data: D) => void;
77
+ /**
78
+ * A named state — mutates the built model INSTANCE (Adonis Lucid: states receive
79
+ * the instance + runtime context, not a raw data object).
80
+ */
81
+ type StateFn<T extends BaseEntity> = (model: T, ctx: FactoryContext) => void;
49
82
 
50
83
  /**
51
84
  * Process-wide counter for stubbed primary keys (Lucid's stub id). Every
@@ -55,6 +88,13 @@ type StateFn<D> = (data: D) => void;
55
88
  */
56
89
  let stubIdCounter = 0;
57
90
 
91
+ /**
92
+ * Global stub-id generator override (Adonis Lucid `Factory.stubId`). Set via
93
+ * {@link Factory.stubId} when models use non-integer primary keys (uuid, etc.);
94
+ * receives the running counter and the instance, returns the id to assign.
95
+ */
96
+ let globalStubId: ((counter: number, model: BaseEntity) => unknown) | undefined;
97
+
58
98
  /** Keys that must never be written through a recursive merge — prototype-pollution guard. */
59
99
  const FORBIDDEN_MERGE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
60
100
 
@@ -85,9 +125,31 @@ function deepMerge(
85
125
  return out;
86
126
  }
87
127
 
128
+ /**
129
+ * A `merge` callback (Adonis Lucid) — receives the built model INSTANCE, the
130
+ * resolved attributes, and the runtime context, e.g.
131
+ * `.merge((user, attributes) => { user.merge(attributes) })`.
132
+ */
133
+ type MergeFn<T extends BaseEntity> = (
134
+ model: T,
135
+ attributes: Record<string, unknown>,
136
+ ctx: FactoryContext,
137
+ ) => void;
138
+
88
139
  export interface FactoryBuilder<T extends BaseEntity> {
89
- /** Override specific fields for the next call (reset after consumption). */
90
- merge(overrides: Partial<Record<string, unknown>>): FactoryBuilder<T>;
140
+ /**
141
+ * Override specific fields for the next call (reset after consumption). Pass an
142
+ * object to shallow-merge, a CALLBACK to mutate the resolved attributes
143
+ * imperatively (Adonis Lucid `merge`), or an ARRAY to override per row on
144
+ * `makeMany`/`createMany` — `merge([{a:1},{a:2}]).createMany(2)` applies index
145
+ * `i` to row `i` (rows past the array length keep the plain defaults).
146
+ */
147
+ merge(
148
+ overrides:
149
+ | Partial<Record<string, unknown>>
150
+ | MergeFn<T>
151
+ | Array<Partial<Record<string, unknown>>>,
152
+ ): FactoryBuilder<T>;
91
153
 
92
154
  /**
93
155
  * Like {@link merge} but deep — nested plain objects are merged key by key
@@ -98,8 +160,12 @@ export interface FactoryBuilder<T extends BaseEntity> {
98
160
  overrides: Partial<Record<string, unknown>>,
99
161
  ): FactoryBuilder<T>;
100
162
 
101
- /** Declare a named variation of this factory, stored on the factory's state map. */
102
- state(name: string, fn: StateFn<Record<string, unknown>>): FactoryBuilder<T>;
163
+ /**
164
+ * Declare a named variation. The callback receives the built model INSTANCE
165
+ * and the runtime context (Adonis Lucid `state`), e.g.
166
+ * `.state('admin', (user) => { user.role = 'admin' })`.
167
+ */
168
+ state(name: string, fn: StateFn<T>): FactoryBuilder<T>;
103
169
 
104
170
  /**
105
171
  * Activate one or more declared states for the NEXT build. Multiple applies
@@ -135,11 +201,17 @@ export interface FactoryBuilder<T extends BaseEntity> {
135
201
  /** Create and persist multiple entities (fires hooks per row). */
136
202
  createMany(count: number, db?: DatabaseConnection): Promise<T[]>;
137
203
 
138
- /** Build the data object without persisting and without instantiating an entity. */
139
- make(): Record<string, unknown>;
204
+ /**
205
+ * Build an entity INSTANCE without persisting it (Adonis Lucid `make`). The
206
+ * instance has NO primary key and `$isPersisted === false` — use it to
207
+ * exercise model logic (computed props, validation) with no DB round trip.
208
+ * For an instance that looks persisted (stub id, `$isPersisted === true`),
209
+ * use {@link makeStubbed}.
210
+ */
211
+ make(): T;
140
212
 
141
- /** Build multiple data objects without persisting. */
142
- makeMany(count: number): Record<string, unknown>[];
213
+ /** Build many un-persisted instances (Adonis Lucid `makeMany`). */
214
+ makeMany(count: number): T[];
143
215
 
144
216
  /**
145
217
  * Build an entity INSTANCE without persisting it (Lucid's `makeStubbed`).
@@ -162,12 +234,10 @@ export interface FactoryBuilder<T extends BaseEntity> {
162
234
  /**
163
235
  * Queue related rows to create together with the next `create`/`createMany`
164
236
  * (Adonis Lucid `.with`). `count` defaults to 1 (ignored past 1 for hasOne).
165
- * The callback receives the related factory to customize it (`merge`/`apply`).
166
- *
167
- * One level deep: the related rows are persisted through the parent's
168
- * relation proxy (which wires the FK / pivot), so a nested `.with()` on the
169
- * callback's factory is not itself applied. `.with()` runs on persistence
170
- * only — `make`/`makeStubbed` ignore it.
237
+ * The callback receives the related factory to customize it `merge`/`apply`,
238
+ * `.pivotAttributes()` for m2m pivot columns, and its own nested `.with()`
239
+ * (arbitrarily deep, Adonis Lucid nested factories). `.with()` runs on
240
+ * persistence only `make`/`makeStubbed` ignore it.
171
241
  */
172
242
  with(
173
243
  name: string,
@@ -176,13 +246,76 @@ export interface FactoryBuilder<T extends BaseEntity> {
176
246
  ): FactoryBuilder<T>;
177
247
 
178
248
  /**
179
- * Register a callback that runs on the built entity INSTANCE before it is
180
- * persisted (Adonis Lucid `.tap`). Multiple taps run in order. Applies to the
181
- * instance-producing paths — `create`/`createMany`/`makeStubbed`/
182
- * `makeStubbedMany` — and is reset after consumption; `make`/`makeMany`
183
- * return plain data, so they ignore it.
249
+ * Register a callback that runs on the built entity INSTANCE (Adonis Lucid
250
+ * `.tap`). Receives the model, the runtime {@link FactoryContext}, and this
251
+ * factory builder. Multiple taps run in order, on every instance-producing
252
+ * path (`make`/`makeMany`/`create`/`createMany`/`makeStubbed*`); reset after
253
+ * consumption.
254
+ */
255
+ tap(
256
+ fn: (model: T, ctx: FactoryContext, builder: FactoryBuilder<T>) => void,
257
+ ): FactoryBuilder<T>;
258
+
259
+ /**
260
+ * Replace the default `new Model()` instantiation (Adonis Lucid `.newUp`).
261
+ * The callback receives the resolved attributes and the runtime context and
262
+ * returns the model instance to use for every subsequent build.
263
+ */
264
+ newUp(
265
+ fn: (attributes: Record<string, unknown>, ctx: FactoryContext) => T,
266
+ ): FactoryBuilder<T>;
267
+
268
+ /**
269
+ * Set pivot columns for the NEXT many-to-many `.with()` link (Adonis Lucid
270
+ * `.pivotAttributes`). Called on the RELATED factory inside a `.with()`
271
+ * callback; the values are written on the pivot row alongside the link. A
272
+ * single object applies to every linked row; pass an ARRAY for different
273
+ * values per row (its length should match the related-row count).
274
+ */
275
+ pivotAttributes(
276
+ attrs: Record<string, unknown> | Array<Record<string, unknown>>,
277
+ ): FactoryBuilder<T>;
278
+
279
+ /**
280
+ * Bind the connection/transaction for subsequent `create`/`createMany` via an
281
+ * options object (Adonis Lucid `.query({ client }) / .query({ connection })`).
282
+ * Sugar over {@link client} / {@link connection}; `client` wins over `connection`.
283
+ */
284
+ query(options: {
285
+ client?: DatabaseConnection;
286
+ connection?: string;
287
+ }): FactoryBuilder<T>;
288
+
289
+ /**
290
+ * Register a lifecycle hook that runs BEFORE the given event (Adonis Lucid
291
+ * factory `before`). `create` fires before the INSERT; `makeStubbed` fires
292
+ * before the stub is finalised (so it can assign the primary key). The
293
+ * callback receives the factory and the model instance. Persistent (declared
294
+ * once, applies to every build).
295
+ */
296
+ before(
297
+ event: "create" | "makeStubbed",
298
+ callback: (
299
+ factory: FactoryBuilder<T>,
300
+ model: T,
301
+ ctx: FactoryContext,
302
+ ) => void,
303
+ ): FactoryBuilder<T>;
304
+
305
+ /**
306
+ * Register a lifecycle hook that runs AFTER the given event (Adonis Lucid
307
+ * factory `after`). `create` fires after the INSERT; `makeStubbed` after the
308
+ * stub is built. `make` fires after an un-persisted `make`/`makeMany` instance
309
+ * is built and tapped (Adonis Lucid `after('make')`). Persistent.
184
310
  */
185
- tap(fn: (entity: T) => void): FactoryBuilder<T>;
311
+ after(
312
+ event: "make" | "create" | "makeStubbed",
313
+ callback: (
314
+ factory: FactoryBuilder<T>,
315
+ model: T,
316
+ ctx: FactoryContext,
317
+ ) => void,
318
+ ): FactoryBuilder<T>;
186
319
  }
187
320
 
188
321
  /**
@@ -193,14 +326,47 @@ export interface FactoryBuilder<T extends BaseEntity> {
193
326
  * name: 'Test User',
194
327
  * }))
195
328
  */
329
+ /**
330
+ * Adonis Lucid `Factory.define(Model, callback).build()` entry point. `define`
331
+ * captures the model and its defaults; `build()` returns the usable factory
332
+ * builder. Equivalent to the one-call {@link factory} shorthand, which stays.
333
+ *
334
+ * const UserFactory = Factory.define(User, ({ faker }) => ({
335
+ * email: faker.internet.email(),
336
+ * })).build()
337
+ */
338
+ export const Factory = {
339
+ define<T extends BaseEntity>(
340
+ entityClass: EntityConstructor<T>,
341
+ defaults: DefaultsFn,
342
+ ): { build(): FactoryBuilder<T> } {
343
+ return { build: () => factory(entityClass, defaults) };
344
+ },
345
+
346
+ /**
347
+ * Override the global stub-id generator (Adonis Lucid `Factory.stubId`). Use
348
+ * when your models have non-integer primary keys — the callback receives the
349
+ * running counter and the instance and returns the id to assign. Pass
350
+ * `null`/nothing to restore the default incrementing integer.
351
+ */
352
+ stubId(generator?: ((counter: number, model: BaseEntity) => unknown) | null) {
353
+ globalStubId = generator ?? undefined;
354
+ },
355
+ };
356
+
196
357
  export function factory<T extends BaseEntity>(
197
358
  entityClass: EntityConstructor<T>,
198
359
  defaults: DefaultsFn,
199
360
  ): FactoryBuilder<T> {
200
361
  // Persistent state — lives across calls.
201
- const states = new Map<string, StateFn<Record<string, unknown>>>();
362
+ const states = new Map<string, StateFn<T>>();
202
363
  // Transient state — resets after every `make`/`create`.
203
364
  let pendingOverrides: Partial<Record<string, unknown>> = {};
365
+ // Per-row overrides from `merge([...])`, applied by row index on makeMany/createMany.
366
+ let pendingOverridesList: Array<Partial<Record<string, unknown>>> | undefined;
367
+ // `merge(callback)` mutators, applied on the built INSTANCE (with the resolved
368
+ // attributes) after instantiation, in call order.
369
+ let pendingMergeFns: MergeFn<T>[] = [];
204
370
  // Recursive overrides are kept apart from the shallow `pendingOverrides`
205
371
  // because they must deep-merge into `defaults()`, which only exists at build
206
372
  // time — a shallow spread here would clobber a whole nested object.
@@ -212,7 +378,22 @@ export function factory<T extends BaseEntity>(
212
378
  const relations = new Map<string, RelationResolver>();
213
379
  let pendingWith: WithRequest[] = [];
214
380
  // Transient `.tap()` callbacks run on the built instance before persistence.
215
- let pendingTap: Array<(entity: T) => void> = [];
381
+ // Adonis Lucid passes the model, the runtime context, and the factory builder.
382
+ let pendingTap: Array<
383
+ (model: T, ctx: FactoryContext, builder: FactoryBuilder<T>) => void
384
+ > = [];
385
+ // Transient pivot columns for the NEXT m2m `.with()` link (Adonis Lucid
386
+ // `.pivotAttributes()`), read by the parent factory's applyRelations. A single
387
+ // object applies to every linked row; an array sets per-row values.
388
+ let pendingPivot:
389
+ | Record<string, unknown>
390
+ | Array<Record<string, unknown>>
391
+ | undefined;
392
+ // Persistent custom instantiation (Adonis Lucid `.newUp`) — replaces
393
+ // `new Model() + setProp` when set.
394
+ let customNewUp:
395
+ | ((attributes: Record<string, unknown>, ctx: FactoryContext) => T)
396
+ | undefined;
216
397
  // Persistent connection bound via `.client()`/`.connection()`, used when
217
398
  // create()/createMany() are called without an explicit `db`.
218
399
  let boundClient: DatabaseConnection | undefined;
@@ -228,12 +409,34 @@ export function factory<T extends BaseEntity>(
228
409
  return conn;
229
410
  };
230
411
 
412
+ // Persistent lifecycle hooks (Adonis Lucid factory before/after), by event.
413
+ type Hook = (
414
+ factory: FactoryBuilder<T>,
415
+ model: T,
416
+ ctx: FactoryContext,
417
+ ) => void;
418
+ const beforeHooks: Record<"create" | "makeStubbed", Hook[]> = {
419
+ create: [],
420
+ makeStubbed: [],
421
+ };
422
+ // `make` has no `before` counterpart in Lucid (nothing to gate before an
423
+ // un-persisted build), so it lives on the after-side only.
424
+ const afterHooks: Record<"make" | "create" | "makeStubbed", Hook[]> = {
425
+ make: [],
426
+ create: [],
427
+ makeStubbed: [],
428
+ };
429
+
231
430
  // Primary-key property, resolved once, for stub-id assignment. Same fallback
232
431
  // as BaseRepository so a model without an explicit `@PrimaryKey` uses `id`.
233
432
  const primaryKey = getPrimaryKey(entityClass) ?? "id";
234
433
 
235
- /** Hydrate a fresh (not-yet-persisted) instance from a data object. */
236
- const newInstance = (data: Record<string, unknown>): T => {
434
+ /** Hydrate a fresh (not-yet-persisted) instance or defer to `.newUp` when set. */
435
+ const newInstance = (
436
+ data: Record<string, unknown>,
437
+ ctx: FactoryContext,
438
+ ): T => {
439
+ if (customNewUp) return customNewUp(data, ctx);
237
440
  const entity = new entityClass();
238
441
  for (const [key, value] of Object.entries(data)) {
239
442
  entity.setProp(key, value);
@@ -241,47 +444,137 @@ export function factory<T extends BaseEntity>(
241
444
  return entity;
242
445
  };
243
446
 
447
+ /** Run every tap with the Lucid `(model, ctx, builder)` signature. */
448
+ const runTaps = (
449
+ taps: Array<
450
+ (model: T, ctx: FactoryContext, builder: FactoryBuilder<T>) => void
451
+ >,
452
+ entity: T,
453
+ ctx: FactoryContext,
454
+ ): void => {
455
+ for (const tap of taps) tap(entity, ctx, builder);
456
+ };
457
+
244
458
  /** Build a stubbed instance: hydrate, run taps, give it a stub id if none was
245
459
  * supplied, then mark persisted so it mirrors a fetched row. Taps run before
246
460
  * `markAsPersisted` so tapped fields land in the clean snapshot. */
461
+ /** Build the runtime context for a build (Adonis Lucid factory `ctx`). */
462
+ const makeCtx = (isStubbed: boolean): FactoryContext => ({
463
+ faker,
464
+ isStubbed,
465
+ $trx: boundClient,
466
+ });
467
+
247
468
  const stub = (
248
469
  data: Record<string, unknown>,
249
- taps: Array<(entity: T) => void>,
470
+ taps: Array<
471
+ (model: T, ctx: FactoryContext, builder: FactoryBuilder<T>) => void
472
+ >,
473
+ mergeFns: MergeFn<T>[],
474
+ stateNames: string[],
475
+ ctx: FactoryContext,
250
476
  ): T => {
251
- const entity = newInstance(data);
252
- for (const tap of taps) tap(entity);
253
- if (data[primaryKey] === undefined) {
254
- entity.setProp(primaryKey, ++stubIdCounter);
477
+ const entity = newInstance(data, ctx);
478
+ applyMergeFns(entity, data, mergeFns, ctx);
479
+ applyStates(entity, stateNames, ctx);
480
+ runTaps(taps, entity, ctx);
481
+ // before('makeStubbed') runs BEFORE the stub id, so a hook can set the PK.
482
+ for (const hook of beforeHooks.makeStubbed) hook(builder, entity, ctx);
483
+ // Assign a stub id only when neither the data NOR a before hook set the PK.
484
+ // A global `Factory.stubId` override generates it (uuid, etc.) when set.
485
+ if (entity[primaryKey] === undefined) {
486
+ const next = ++stubIdCounter;
487
+ entity.setProp(
488
+ primaryKey,
489
+ globalStubId ? globalStubId(next, entity) : next,
490
+ );
255
491
  }
256
492
  entity.markAsPersisted();
493
+ for (const hook of afterHooks.makeStubbed) hook(builder, entity, ctx);
257
494
  return entity;
258
495
  };
259
496
 
260
- const buildData = (): Record<string, unknown> => {
497
+ const buildData = (
498
+ ctx: FactoryContext,
499
+ index = 0,
500
+ ): Record<string, unknown> => {
261
501
  let data: Record<string, unknown> = {
262
- ...defaults({ faker }),
502
+ ...defaults(ctx),
263
503
  ...pendingOverrides,
504
+ // Per-row array overrides (merge([...])) win over the shared object.
505
+ ...(pendingOverridesList?.[index] ?? {}),
264
506
  };
265
507
  // Deep overrides layer on top of the shallow one so nested defaults survive.
266
508
  if (Object.keys(pendingRecursive).length > 0) {
267
509
  data = deepMerge(data, pendingRecursive);
268
510
  }
269
- for (const name of pendingStates) {
511
+ return data;
512
+ };
513
+
514
+ /**
515
+ * Run `merge(callback)` mutators on the built INSTANCE (Adonis Lucid passes the
516
+ * model, the resolved attributes, and the context). Runs before states/taps.
517
+ */
518
+ const applyMergeFns = (
519
+ entity: T,
520
+ attributes: Record<string, unknown>,
521
+ mergeFns: MergeFn<T>[],
522
+ ctx: FactoryContext,
523
+ ): void => {
524
+ for (const fn of mergeFns) fn(entity, attributes, ctx);
525
+ };
526
+
527
+ /**
528
+ * Run the named states on the built INSTANCE (Adonis Lucid). Separated from
529
+ * {@link buildData} because states mutate the model, not the raw attributes.
530
+ */
531
+ const applyStates = (
532
+ entity: T,
533
+ stateNames: string[],
534
+ ctx: FactoryContext,
535
+ ): void => {
536
+ for (const name of stateNames) {
270
537
  const fn = states.get(name);
271
- if (!fn)
538
+ if (!fn) {
272
539
  throw new Error(
273
540
  `Factory state '${name}' is not defined on ${entityClass.name}Factory`,
274
541
  );
275
- fn(data);
542
+ }
543
+ fn(entity, ctx);
276
544
  }
545
+ };
546
+
547
+ /** Build one raw row and clear transient state — the persistence paths
548
+ * (`create`/`createMany`) need the plain object, not an instance. */
549
+ const consumeData = (ctx: FactoryContext): Record<string, unknown> => {
550
+ const data = buildData(ctx);
551
+ resetPending();
277
552
  return data;
278
553
  };
279
554
 
555
+ /** Build `count` raw rows (distinct faker/Date values per row), then reset. */
556
+ const consumeDataMany = (
557
+ count: number,
558
+ ctx: FactoryContext,
559
+ ): Record<string, unknown>[] => {
560
+ const rows: Record<string, unknown>[] = [];
561
+ for (let i = 0; i < count; i++) rows.push(buildData(ctx, i));
562
+ resetPending();
563
+ return rows;
564
+ };
565
+
280
566
  const resetPending = (): void => {
281
567
  pendingOverrides = {};
568
+ pendingOverridesList = undefined;
569
+ pendingMergeFns = [];
282
570
  pendingRecursive = {};
283
571
  pendingStates = [];
284
572
  pendingTap = [];
573
+ pendingPivot = undefined;
574
+ // Also clear queued relations, so `.with(...).make()` — which ignores
575
+ // relations — cannot leak them into the NEXT create() (create/createMany
576
+ // capture the queue before make() runs, so they are unaffected).
577
+ pendingWith = [];
285
578
  };
286
579
 
287
580
  /**
@@ -293,26 +586,77 @@ export function factory<T extends BaseEntity>(
293
586
  const persist = async (
294
587
  repo: BaseRepository<T>,
295
588
  data: Record<string, unknown>,
296
- taps: Array<(entity: T) => void>,
589
+ taps: Array<
590
+ (model: T, ctx: FactoryContext, builder: FactoryBuilder<T>) => void
591
+ >,
592
+ mergeFns: MergeFn<T>[],
593
+ stateNames: string[],
594
+ ctx: FactoryContext,
297
595
  ): Promise<T> => {
298
- if (taps.length === 0) return repo.create(data);
299
- const entity = newInstance(data);
300
- for (const tap of taps) tap(entity);
596
+ if (
597
+ taps.length === 0 &&
598
+ mergeFns.length === 0 &&
599
+ stateNames.length === 0 &&
600
+ beforeHooks.create.length === 0 &&
601
+ afterHooks.create.length === 0 &&
602
+ !customNewUp
603
+ ) {
604
+ return repo.create(data);
605
+ }
606
+ const entity = newInstance(data, ctx);
607
+ applyMergeFns(entity, data, mergeFns, ctx);
608
+ applyStates(entity, stateNames, ctx);
609
+ runTaps(taps, entity, ctx);
610
+ for (const hook of beforeHooks.create) hook(builder, entity, ctx);
301
611
  await repo.save(entity);
612
+ for (const hook of afterHooks.create) hook(builder, entity, ctx);
302
613
  return entity;
303
614
  };
304
615
 
616
+ /**
617
+ * Persist a parent and its queued `.with()` relations. With relations, the
618
+ * whole graph runs in a managed transaction (Adonis Lucid: if a related write
619
+ * fails the parent insert rolls back too); the parent is created through a
620
+ * `useTransaction`-bound repo so its `related()` proxies also use the trx.
621
+ * With no relations there's nothing to make atomic — a single insert already
622
+ * is — so it stays on the plain connection.
623
+ */
624
+ const persistWithRelations = async (
625
+ conn: DatabaseConnection,
626
+ withReqs: WithRequest[],
627
+ recursive: Record<string, unknown>,
628
+ build: (repo: BaseRepository<T>, ctx: FactoryContext) => Promise<T>,
629
+ ): Promise<T> => {
630
+ if (withReqs.length === 0) {
631
+ // ctx.$trx is the connection actually used to persist (an explicit
632
+ // create(db) client, or the bound one) — not just the bound client.
633
+ return build(new BaseRepository(entityClass, conn), {
634
+ faker,
635
+ isStubbed: false,
636
+ $trx: conn,
637
+ });
638
+ }
639
+ return transaction(conn, async (trx) => {
640
+ const repo = new BaseRepository(entityClass, conn).useTransaction(trx);
641
+ const entity = await build(repo, { faker, isStubbed: false, $trx: trx });
642
+ await applyRelations(entity, withReqs, trx, recursive);
643
+ return entity;
644
+ });
645
+ };
646
+
305
647
  /**
306
648
  * Persist the queued `.with()` relations for one just-created parent, routing
307
649
  * through the parent's relation proxy so the FK / pivot is wired by the
308
650
  * already-tested relation-write code (not re-derived here).
309
651
  */
310
652
  const applyRelations = async (
311
- parent: T,
653
+ parent: BaseEntity,
312
654
  reqs: WithRequest[],
313
655
  db: DatabaseConnection,
656
+ recursive?: Record<string, unknown>,
314
657
  ): Promise<void> => {
315
658
  const meta = getRelationMetadata(entityClass);
659
+ const hasRecursive = recursive && Object.keys(recursive).length > 0;
316
660
  for (const req of reqs) {
317
661
  const relMeta = meta.find((r) => r.propertyKey === req.name);
318
662
  if (!relMeta) {
@@ -327,23 +671,60 @@ export function factory<T extends BaseEntity>(
327
671
  );
328
672
  }
329
673
  const childFactory = resolver();
674
+ // mergeRecursive cascades the SAME overrides onto every related factory
675
+ // (Adonis Lucid) — applied before the callback so an explicit merge in
676
+ // the callback still wins.
677
+ if (hasRecursive && recursive) childFactory.mergeRecursive(recursive);
330
678
  if (req.callback) req.callback(childFactory);
679
+ const childInternals = factoryInternals.get(childFactory);
331
680
 
332
681
  const proxy = parent.related(req.name);
682
+ if (proxy.type === "belongsTo") {
683
+ // FK lives on the parent: create() runs the owner's OWN with-graph,
684
+ // then associate re-saves the parent with the FK set.
685
+ const owner = await childFactory.create(db);
686
+ await proxy.associate(owner);
687
+ continue;
688
+ }
689
+
690
+ // The proxy-persisted branches bypass the child factory's create() path,
691
+ // so its queued nested `.with()` must be captured before make() clears it,
692
+ // then recursed onto each persisted child (Adonis Lucid nested factories:
693
+ // `post.with('comments', 5)` inside the callback).
694
+ const nestedWith = childInternals?.consumeWith() ?? [];
695
+ // Pivot columns from a `.pivotAttributes()` in the callback — captured
696
+ // now, before make() clears the child's transient state.
697
+ const nestedPivot = childInternals?.consumePivot();
698
+ const recurse = async (children: BaseEntity[]): Promise<void> => {
699
+ if (nestedWith.length === 0 || !childInternals) return;
700
+ for (const child of children) {
701
+ // Cascade the recursive overrides further down the graph.
702
+ await childInternals.applyRelations(child, nestedWith, db, recursive);
703
+ }
704
+ };
705
+
333
706
  if (proxy.type === "hasMany") {
334
- await proxy.createMany(childFactory.makeMany(req.count));
707
+ await recurse(await proxy.createMany(childFactory.makeMany(req.count)));
335
708
  } else if (proxy.type === "hasOne") {
336
- await proxy.create(childFactory.make());
709
+ await recurse([await proxy.create(childFactory.make())]);
337
710
  } else if (proxy.type === "manyToMany") {
338
- // The m2m proxy's create() inserts the related row AND the pivot link.
339
- for (const row of childFactory.makeMany(req.count)) {
340
- await proxy.create(row);
711
+ // The m2m proxy's create() inserts the related row AND the pivot link,
712
+ // with the `.pivotAttributes()` columns on the pivot row. An array of
713
+ // pivot attrs applies per-row (`pivot[i]`); a single object, to all.
714
+ const rows = childFactory.makeMany(req.count);
715
+ // Adonis Lucid: an array's length must match the related-row count —
716
+ // reject a mismatch rather than silently leave rows without pivot data.
717
+ if (Array.isArray(nestedPivot) && nestedPivot.length !== rows.length) {
718
+ throw new Error(
719
+ `Factory .with('${req.name}'): pivotAttributes array length (${nestedPivot.length}) must match the related-row count (${rows.length}).`,
720
+ );
721
+ }
722
+ for (let i = 0; i < rows.length; i++) {
723
+ const pivot = Array.isArray(nestedPivot)
724
+ ? nestedPivot[i]
725
+ : nestedPivot;
726
+ await recurse([await proxy.create(rows[i], pivot)]);
341
727
  }
342
- } else if (proxy.type === "belongsTo") {
343
- // FK lives on the parent: create the owner, then associate re-saves
344
- // the parent with the FK set.
345
- const owner = await childFactory.create(db);
346
- await proxy.associate(owner);
347
728
  } else {
348
729
  throw new Error(
349
730
  `Factory .with('${req.name}'): '${relMeta.type}' relations are not supported`,
@@ -354,7 +735,13 @@ export function factory<T extends BaseEntity>(
354
735
 
355
736
  const builder: FactoryBuilder<T> = {
356
737
  merge(overrides) {
357
- pendingOverrides = { ...pendingOverrides, ...overrides };
738
+ if (typeof overrides === "function") {
739
+ pendingMergeFns.push(overrides);
740
+ } else if (Array.isArray(overrides)) {
741
+ pendingOverridesList = overrides;
742
+ } else {
743
+ pendingOverrides = { ...pendingOverrides, ...overrides };
744
+ }
358
745
  return builder;
359
746
  },
360
747
 
@@ -388,6 +775,31 @@ export function factory<T extends BaseEntity>(
388
775
  return builder;
389
776
  },
390
777
 
778
+ newUp(fn) {
779
+ customNewUp = fn;
780
+ return builder;
781
+ },
782
+
783
+ pivotAttributes(attrs) {
784
+ // An array (per-row values) replaces wholesale; objects merge (repeated
785
+ // `.pivotAttributes({...})` accumulate, matching the single-object case).
786
+ pendingPivot =
787
+ Array.isArray(attrs) || Array.isArray(pendingPivot)
788
+ ? attrs
789
+ : { ...pendingPivot, ...attrs };
790
+ return builder;
791
+ },
792
+
793
+ before(event, callback) {
794
+ beforeHooks[event].push(callback);
795
+ return builder;
796
+ },
797
+
798
+ after(event, callback) {
799
+ afterHooks[event].push(callback);
800
+ return builder;
801
+ },
802
+
391
803
  client(connection) {
392
804
  boundClient = connection;
393
805
  return builder;
@@ -404,19 +816,54 @@ export function factory<T extends BaseEntity>(
404
816
  return builder;
405
817
  },
406
818
 
819
+ query(options) {
820
+ if (options.client) {
821
+ boundClient = options.client;
822
+ } else if (options.connection) {
823
+ const conn = getConnection(options.connection);
824
+ if (!conn) {
825
+ throw new Error(
826
+ `Factory ${entityClass.name}: no connection registered under '${options.connection}'.`,
827
+ );
828
+ }
829
+ boundClient = conn;
830
+ }
831
+ return builder;
832
+ },
833
+
407
834
  make() {
408
- const data = buildData();
835
+ // Lucid `make`: an UN-persisted instance (no PK, `$isPersisted` false).
836
+ // Taps run on it, like the other instance-producing paths.
837
+ const taps = pendingTap;
838
+ const mergeFns = pendingMergeFns;
839
+ const stateNames = pendingStates;
840
+ const ctx = makeCtx(false);
841
+ const data = buildData(ctx);
842
+ const entity = newInstance(data, ctx);
843
+ applyMergeFns(entity, data, mergeFns, ctx);
844
+ applyStates(entity, stateNames, ctx);
845
+ runTaps(taps, entity, ctx);
846
+ for (const hook of afterHooks.make) hook(builder, entity, ctx);
409
847
  resetPending();
410
- return data;
848
+ return entity;
411
849
  },
412
850
 
413
851
  makeMany(count) {
414
852
  // Re-evaluate defaults for each row so `Date.now()` / faker generate
415
- // distinct values. `buildData()` reads (never mutates) the pending
416
- // overrides/states, so they stay stable across iterations on their own.
417
- const rows: Record<string, unknown>[] = [];
853
+ // distinct values, each hydrated into its own un-persisted instance.
854
+ const taps = pendingTap;
855
+ const mergeFns = pendingMergeFns;
856
+ const stateNames = pendingStates;
857
+ const ctx = makeCtx(false);
858
+ const rows: T[] = [];
418
859
  for (let i = 0; i < count; i++) {
419
- rows.push(buildData());
860
+ const data = buildData(ctx, i);
861
+ const entity = newInstance(data, ctx);
862
+ applyMergeFns(entity, data, mergeFns, ctx);
863
+ applyStates(entity, stateNames, ctx);
864
+ runTaps(taps, entity, ctx);
865
+ for (const hook of afterHooks.make) hook(builder, entity, ctx);
866
+ rows.push(entity);
420
867
  }
421
868
  resetPending();
422
869
  return rows;
@@ -424,8 +871,10 @@ export function factory<T extends BaseEntity>(
424
871
 
425
872
  makeStubbed() {
426
873
  const taps = pendingTap;
427
- const data = buildData();
428
- const entity = stub(data, taps);
874
+ const mergeFns = pendingMergeFns;
875
+ const stateNames = pendingStates;
876
+ const ctx = makeCtx(true);
877
+ const entity = stub(buildData(ctx), taps, mergeFns, stateNames, ctx);
429
878
  resetPending();
430
879
  return entity;
431
880
  },
@@ -434,45 +883,76 @@ export function factory<T extends BaseEntity>(
434
883
  // Re-evaluate defaults per row (distinct Date.now()/faker values), same
435
884
  // as makeMany; each row then gets its own stub id + taps via `stub`.
436
885
  const taps = pendingTap;
886
+ const mergeFns = pendingMergeFns;
887
+ const stateNames = pendingStates;
888
+ const ctx = makeCtx(true);
437
889
  const rows: T[] = [];
438
890
  for (let i = 0; i < count; i++) {
439
- rows.push(stub(buildData(), taps));
891
+ rows.push(stub(buildData(ctx, i), taps, mergeFns, stateNames, ctx));
440
892
  }
441
893
  resetPending();
442
894
  return rows;
443
895
  },
444
896
 
445
897
  async create(db) {
446
- const conn = resolveConnection(db);
447
- // Capture + clear the relation/tap queues before make() (which resets
448
- // pending state) so they aren't lost and a later create() starts clean.
898
+ // Capture + clear the relation/tap queues, then consume the raw data
899
+ // (which resets the rest of the pending state) BEFORE resolveConnection,
900
+ // so a missing connection can't throw with pending overrides/states/with/
901
+ // tap still dirty and leak them into the next create().
449
902
  const withReqs = pendingWith;
450
903
  pendingWith = [];
451
904
  const taps = pendingTap;
452
- const data = builder.make();
453
- const repo = new BaseRepository(entityClass, conn);
454
- const entity = await persist(repo, data, taps);
455
- if (withReqs.length > 0) await applyRelations(entity, withReqs, conn);
456
- return entity;
905
+ const mergeFns = pendingMergeFns;
906
+ const stateNames = pendingStates;
907
+ // Snapshot the recursive overrides BEFORE consumeData resets them — they
908
+ // cascade onto every related factory (Adonis Lucid `mergeRecursive`).
909
+ const recursive = { ...pendingRecursive };
910
+ const ctx = makeCtx(false);
911
+ const data = consumeData(ctx);
912
+ const conn = resolveConnection(db);
913
+ return persistWithRelations(conn, withReqs, recursive, (repo, txCtx) =>
914
+ persist(repo, data, taps, mergeFns, stateNames, txCtx),
915
+ );
457
916
  },
458
917
 
459
918
  async createMany(count, db) {
460
- const conn = resolveConnection(db);
461
919
  const withReqs = pendingWith;
462
920
  pendingWith = [];
463
921
  const taps = pendingTap;
464
- const rows = builder.makeMany(count);
465
- const repo = new BaseRepository(entityClass, conn);
922
+ const mergeFns = pendingMergeFns;
923
+ const stateNames = pendingStates;
924
+ const recursive = { ...pendingRecursive };
925
+ const ctx = makeCtx(false);
926
+ const rows = consumeDataMany(count, ctx);
927
+ const conn = resolveConnection(db);
466
928
  const created: T[] = [];
467
929
  for (const data of rows) {
468
- const entity = await persist(repo, data, taps);
469
- // Each created parent gets its own related rows (Lucid semantics).
470
- if (withReqs.length > 0) await applyRelations(entity, withReqs, conn);
471
- created.push(entity);
930
+ // Each created parent + its relations are atomic (Lucid semantics).
931
+ created.push(
932
+ await persistWithRelations(conn, withReqs, recursive, (repo, txCtx) =>
933
+ persist(repo, data, taps, mergeFns, stateNames, txCtx),
934
+ ),
935
+ );
472
936
  }
473
937
  return created;
474
938
  },
475
939
  };
476
940
 
941
+ // Register this builder's internals so a PARENT factory can drive nested
942
+ // `.with()` on it (kept off the public type, keyed by the builder object).
943
+ factoryInternals.set(builder, {
944
+ consumeWith() {
945
+ const reqs = pendingWith;
946
+ pendingWith = [];
947
+ return reqs;
948
+ },
949
+ consumePivot() {
950
+ const pivot = pendingPivot;
951
+ pendingPivot = undefined;
952
+ return pivot;
953
+ },
954
+ applyRelations,
955
+ });
956
+
477
957
  return builder;
478
958
  }