@c9up/atlas 0.1.19 → 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 (250) hide show
  1. package/README.md +63 -14
  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 +66 -7
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +205 -39
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +181 -5
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js +339 -33
  14. package/dist/BaseEntity.js.map +1 -1
  15. package/dist/BaseModel.d.ts +127 -0
  16. package/dist/BaseModel.d.ts.map +1 -0
  17. package/dist/BaseModel.js +250 -0
  18. package/dist/BaseModel.js.map +1 -0
  19. package/dist/BaseRepository.d.ts +81 -15
  20. package/dist/BaseRepository.d.ts.map +1 -1
  21. package/dist/BaseRepository.js +1433 -387
  22. package/dist/BaseRepository.js.map +1 -1
  23. package/dist/ConnectionManager.d.ts +106 -0
  24. package/dist/ConnectionManager.d.ts.map +1 -0
  25. package/dist/ConnectionManager.js +228 -0
  26. package/dist/ConnectionManager.js.map +1 -0
  27. package/dist/ModelQuery.d.ts +577 -41
  28. package/dist/ModelQuery.d.ts.map +1 -1
  29. package/dist/ModelQuery.js +2188 -291
  30. package/dist/ModelQuery.js.map +1 -1
  31. package/dist/Transaction.d.ts +71 -1
  32. package/dist/Transaction.d.ts.map +1 -1
  33. package/dist/Transaction.js +138 -24
  34. package/dist/Transaction.js.map +1 -1
  35. package/dist/adapters/NapiDbAdapter.d.ts +44 -4
  36. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  37. package/dist/adapters/NapiDbAdapter.js +131 -13
  38. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  39. package/dist/console/contract.d.ts +62 -0
  40. package/dist/console/contract.d.ts.map +1 -0
  41. package/dist/console/contract.js +38 -0
  42. package/dist/console/contract.js.map +1 -0
  43. package/dist/console/factoryCommands.d.ts +23 -0
  44. package/dist/console/factoryCommands.d.ts.map +1 -0
  45. package/dist/console/factoryCommands.js +62 -0
  46. package/dist/console/factoryCommands.js.map +1 -0
  47. package/dist/console/migrationCommands.d.ts +78 -0
  48. package/dist/console/migrationCommands.d.ts.map +1 -0
  49. package/dist/console/migrationCommands.js +327 -0
  50. package/dist/console/migrationCommands.js.map +1 -0
  51. package/dist/console/schemaCheckCommand.d.ts +11 -17
  52. package/dist/console/schemaCheckCommand.d.ts.map +1 -1
  53. package/dist/console/schemaCheckCommand.js +21 -15
  54. package/dist/console/schemaCheckCommand.js.map +1 -1
  55. package/dist/console/schemaDumpCommand.d.ts +30 -0
  56. package/dist/console/schemaDumpCommand.d.ts.map +1 -0
  57. package/dist/console/schemaDumpCommand.js +69 -0
  58. package/dist/console/schemaDumpCommand.js.map +1 -0
  59. package/dist/console/schemaGenerateCommand.d.ts +100 -0
  60. package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
  61. package/dist/console/schemaGenerateCommand.js +246 -0
  62. package/dist/console/schemaGenerateCommand.js.map +1 -0
  63. package/dist/console/seederCommands.d.ts +46 -0
  64. package/dist/console/seederCommands.d.ts.map +1 -0
  65. package/dist/console/seederCommands.js +136 -0
  66. package/dist/console/seederCommands.js.map +1 -0
  67. package/dist/decorators/entity.d.ts +50 -8
  68. package/dist/decorators/entity.d.ts.map +1 -1
  69. package/dist/decorators/entity.js +42 -2
  70. package/dist/decorators/entity.js.map +1 -1
  71. package/dist/decorators/hooks.d.ts +3 -3
  72. package/dist/decorators/hooks.d.ts.map +1 -1
  73. package/dist/decorators/hooks.js.map +1 -1
  74. package/dist/events.d.ts +69 -0
  75. package/dist/events.d.ts.map +1 -0
  76. package/dist/events.js +82 -0
  77. package/dist/events.js.map +1 -0
  78. package/dist/factories.d.ts +8 -0
  79. package/dist/factories.d.ts.map +1 -0
  80. package/dist/factories.js +8 -0
  81. package/dist/factories.js.map +1 -0
  82. package/dist/index.d.ts +15 -5
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +14 -3
  85. package/dist/index.js.map +1 -1
  86. package/dist/lucid-schema.d.ts +15 -0
  87. package/dist/lucid-schema.d.ts.map +1 -0
  88. package/dist/lucid-schema.js +16 -0
  89. package/dist/lucid-schema.js.map +1 -0
  90. package/dist/metadata-keys.d.ts +3 -2
  91. package/dist/metadata-keys.d.ts.map +1 -1
  92. package/dist/naming/NamingStrategy.d.ts +7 -0
  93. package/dist/naming/NamingStrategy.d.ts.map +1 -1
  94. package/dist/naming/NamingStrategy.js +16 -0
  95. package/dist/naming/NamingStrategy.js.map +1 -1
  96. package/dist/orm.d.ts +14 -0
  97. package/dist/orm.d.ts.map +1 -0
  98. package/dist/orm.js +14 -0
  99. package/dist/orm.js.map +1 -0
  100. package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
  101. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
  102. package/dist/query/DatabaseQueryBuilder.js +1798 -0
  103. package/dist/query/DatabaseQueryBuilder.js.map +1 -0
  104. package/dist/query/DmlBuilder.d.ts +62 -0
  105. package/dist/query/DmlBuilder.d.ts.map +1 -0
  106. package/dist/query/DmlBuilder.js +84 -0
  107. package/dist/query/DmlBuilder.js.map +1 -0
  108. package/dist/query/QueryBuilder.d.ts +5 -0
  109. package/dist/query/QueryBuilder.d.ts.map +1 -1
  110. package/dist/query/QueryBuilder.js +7 -0
  111. package/dist/query/QueryBuilder.js.map +1 -1
  112. package/dist/query/RawQueryBuilder.d.ts +47 -0
  113. package/dist/query/RawQueryBuilder.d.ts.map +1 -0
  114. package/dist/query/RawQueryBuilder.js +138 -0
  115. package/dist/query/RawQueryBuilder.js.map +1 -0
  116. package/dist/query/interpolate.d.ts +45 -0
  117. package/dist/query/interpolate.d.ts.map +1 -0
  118. package/dist/query/interpolate.js +51 -0
  119. package/dist/query/interpolate.js.map +1 -0
  120. package/dist/query/native.d.ts +8 -0
  121. package/dist/query/native.d.ts.map +1 -1
  122. package/dist/query/native.js +14 -0
  123. package/dist/query/native.js.map +1 -1
  124. package/dist/query/operators.d.ts +4 -0
  125. package/dist/query/operators.d.ts.map +1 -0
  126. package/dist/query/operators.js +27 -0
  127. package/dist/query/operators.js.map +1 -0
  128. package/dist/schema/Migration.d.ts +50 -3
  129. package/dist/schema/Migration.d.ts.map +1 -1
  130. package/dist/schema/Migration.js +60 -24
  131. package/dist/schema/Migration.js.map +1 -1
  132. package/dist/schema/MigrationRunner.d.ts +95 -34
  133. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  134. package/dist/schema/MigrationRunner.js +523 -68
  135. package/dist/schema/MigrationRunner.js.map +1 -1
  136. package/dist/schema/Schema.d.ts +114 -0
  137. package/dist/schema/Schema.d.ts.map +1 -1
  138. package/dist/schema/Schema.js +272 -3
  139. package/dist/schema/Schema.js.map +1 -1
  140. package/dist/schema/SchemaDumper.d.ts +121 -0
  141. package/dist/schema/SchemaDumper.d.ts.map +1 -0
  142. package/dist/schema/SchemaDumper.js +365 -0
  143. package/dist/schema/SchemaDumper.js.map +1 -0
  144. package/dist/schema/Seeder.d.ts +13 -0
  145. package/dist/schema/Seeder.d.ts.map +1 -1
  146. package/dist/schema/Seeder.js +28 -5
  147. package/dist/schema/Seeder.js.map +1 -1
  148. package/dist/schema/TableBuilder.d.ts +274 -16
  149. package/dist/schema/TableBuilder.d.ts.map +1 -1
  150. package/dist/schema/TableBuilder.js +656 -52
  151. package/dist/schema/TableBuilder.js.map +1 -1
  152. package/dist/schema/catalog.d.ts +71 -0
  153. package/dist/schema/catalog.d.ts.map +1 -0
  154. package/dist/schema/catalog.js +158 -0
  155. package/dist/schema/catalog.js.map +1 -0
  156. package/dist/schema/introspect.d.ts +1 -1
  157. package/dist/schema/introspect.d.ts.map +1 -1
  158. package/dist/schema/introspect.js +26 -11
  159. package/dist/schema/introspect.js.map +1 -1
  160. package/dist/schema/types.d.ts +150 -1
  161. package/dist/schema/types.d.ts.map +1 -1
  162. package/dist/schema/types.js +11 -0
  163. package/dist/schema/types.js.map +1 -1
  164. package/dist/seeders.d.ts +7 -0
  165. package/dist/seeders.d.ts.map +1 -0
  166. package/dist/seeders.js +7 -0
  167. package/dist/seeders.js.map +1 -0
  168. package/dist/services/db.d.ts +122 -5
  169. package/dist/services/db.d.ts.map +1 -1
  170. package/dist/services/db.js +187 -14
  171. package/dist/services/db.js.map +1 -1
  172. package/dist/testing/DatabaseCleanup.d.ts +33 -7
  173. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  174. package/dist/testing/DatabaseCleanup.js +54 -24
  175. package/dist/testing/DatabaseCleanup.js.map +1 -1
  176. package/dist/testing/DbAssertions.d.ts +48 -0
  177. package/dist/testing/DbAssertions.d.ts.map +1 -0
  178. package/dist/testing/DbAssertions.js +80 -0
  179. package/dist/testing/DbAssertions.js.map +1 -0
  180. package/dist/testing/Factory.d.ts +166 -15
  181. package/dist/testing/Factory.d.ts.map +1 -1
  182. package/dist/testing/Factory.js +497 -23
  183. package/dist/testing/Factory.js.map +1 -1
  184. package/dist/testing/TestUtils.d.ts +49 -0
  185. package/dist/testing/TestUtils.d.ts.map +1 -0
  186. package/dist/testing/TestUtils.js +79 -0
  187. package/dist/testing/TestUtils.js.map +1 -0
  188. package/dist/testing/index.d.ts +3 -1
  189. package/dist/testing/index.d.ts.map +1 -1
  190. package/dist/testing/index.js +3 -1
  191. package/dist/testing/index.js.map +1 -1
  192. package/dist/types/relations.d.ts +31 -0
  193. package/dist/types/relations.d.ts.map +1 -0
  194. package/dist/types/relations.js +20 -0
  195. package/dist/types/relations.js.map +1 -0
  196. package/index.darwin-arm64.node +0 -0
  197. package/index.darwin-x64.node +0 -0
  198. package/index.linux-arm64-gnu.node +0 -0
  199. package/index.linux-x64-gnu.node +0 -0
  200. package/index.win32-x64-msvc.node +0 -0
  201. package/package.json +32 -2
  202. package/scripts/guard-publish.mjs +15 -0
  203. package/src/AtlasProvider.ts +305 -52
  204. package/src/BaseEntity.ts +460 -41
  205. package/src/BaseModel.ts +461 -0
  206. package/src/BaseRepository.ts +1710 -421
  207. package/src/ConnectionManager.ts +298 -0
  208. package/src/ModelQuery.ts +2998 -335
  209. package/src/Transaction.ts +227 -30
  210. package/src/adapters/NapiDbAdapter.ts +224 -17
  211. package/src/console/contract.ts +96 -0
  212. package/src/console/factoryCommands.ts +73 -0
  213. package/src/console/migrationCommands.ts +448 -0
  214. package/src/console/schemaCheckCommand.ts +26 -22
  215. package/src/console/schemaDumpCommand.ts +96 -0
  216. package/src/console/schemaGenerateCommand.ts +380 -0
  217. package/src/console/seederCommands.ts +174 -0
  218. package/src/decorators/entity.ts +71 -8
  219. package/src/decorators/hooks.ts +5 -3
  220. package/src/events.ts +117 -0
  221. package/src/factories.ts +10 -0
  222. package/src/index.ts +69 -3
  223. package/src/lucid-schema.ts +22 -0
  224. package/src/metadata-keys.ts +3 -2
  225. package/src/naming/NamingStrategy.ts +23 -0
  226. package/src/orm.ts +13 -0
  227. package/src/query/DatabaseQueryBuilder.ts +2724 -0
  228. package/src/query/DmlBuilder.ts +131 -0
  229. package/src/query/QueryBuilder.ts +8 -0
  230. package/src/query/RawQueryBuilder.ts +173 -0
  231. package/src/query/interpolate.ts +73 -0
  232. package/src/query/native.ts +29 -0
  233. package/src/query/operators.ts +27 -0
  234. package/src/schema/Migration.ts +71 -3
  235. package/src/schema/MigrationRunner.ts +676 -69
  236. package/src/schema/Schema.ts +359 -3
  237. package/src/schema/SchemaDumper.ts +518 -0
  238. package/src/schema/Seeder.ts +47 -10
  239. package/src/schema/TableBuilder.ts +783 -52
  240. package/src/schema/catalog.ts +236 -0
  241. package/src/schema/introspect.ts +26 -9
  242. package/src/schema/types.ts +137 -2
  243. package/src/seeders.ts +16 -0
  244. package/src/services/db.ts +361 -21
  245. package/src/testing/DatabaseCleanup.ts +78 -25
  246. package/src/testing/DbAssertions.ts +127 -0
  247. package/src/testing/Factory.ts +837 -40
  248. package/src/testing/TestUtils.ts +123 -0
  249. package/src/testing/index.ts +12 -1
  250. package/src/types/relations.ts +43 -0
@@ -14,20 +14,158 @@
14
14
  * @implements MISS-18, Story 32.13
15
15
  */
16
16
 
17
+ import { type Faker, faker } from "@faker-js/faker";
17
18
  import type { BaseEntity } from "../BaseEntity.js";
18
19
  import { BaseRepository, type DatabaseConnection } from "../BaseRepository.js";
20
+ import { getPrimaryKey, getRelationMetadata } from "../decorators/entity.js";
21
+ import { getConnection } from "../services/db.js";
22
+ import { transaction } from "../Transaction.js";
19
23
 
20
24
  type EntityConstructor<T extends BaseEntity> = new () => T;
21
25
 
22
- /** A named state mutates an in-progress data object in place. */
23
- type StateFn<D> = (data: D) => void;
26
+ /** Resolves the factory that builds a related model (Lucid `.relation`). */
27
+ type RelationResolver = () => FactoryBuilder<BaseEntity>;
28
+
29
+ /** A queued `.with()` request, applied on the next create()/createMany(). */
30
+ interface WithRequest {
31
+ name: string;
32
+ count: number;
33
+ callback?: (factory: FactoryBuilder<BaseEntity>) => void;
34
+ }
35
+
36
+ /**
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.
67
+ */
68
+ export interface FactoryContext {
69
+ faker: Faker;
70
+ isStubbed: boolean;
71
+ $trx?: DatabaseConnection;
72
+ }
73
+
74
+ /** Attributes callback — receives {@link FactoryContext}, returns the row shape. */
75
+ type DefaultsFn = (ctx: FactoryContext) => Record<string, unknown>;
76
+
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;
82
+
83
+ /**
84
+ * Process-wide counter for stubbed primary keys (Lucid's stub id). Every
85
+ * `makeStubbed*` build with no explicit PK gets the next value, so stubbed
86
+ * instances have stable, distinct, DB-free identifiers — enough to look
87
+ * persisted and to key relations/serialization without a round trip.
88
+ */
89
+ let stubIdCounter = 0;
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
+
98
+ /** Keys that must never be written through a recursive merge — prototype-pollution guard. */
99
+ const FORBIDDEN_MERGE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
100
+
101
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
102
+ if (value === null || typeof value !== "object") return false;
103
+ const proto = Object.getPrototypeOf(value);
104
+ return proto === Object.prototype || proto === null;
105
+ }
106
+
107
+ /**
108
+ * Deep-merge `source` into `target` (Lucid `mergeRecursive`). Plain objects are
109
+ * merged key by key; arrays and every non-plain value replace wholesale. Returns
110
+ * a fresh object — neither argument is mutated. Skips prototype-pollution keys.
111
+ */
112
+ function deepMerge(
113
+ target: Record<string, unknown>,
114
+ source: Record<string, unknown>,
115
+ ): Record<string, unknown> {
116
+ const out: Record<string, unknown> = { ...target };
117
+ for (const [key, value] of Object.entries(source)) {
118
+ if (FORBIDDEN_MERGE_KEYS.has(key)) continue;
119
+ const existing = out[key];
120
+ out[key] =
121
+ isPlainObject(existing) && isPlainObject(value)
122
+ ? deepMerge(existing, value)
123
+ : value;
124
+ }
125
+ return out;
126
+ }
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;
24
138
 
25
139
  export interface FactoryBuilder<T extends BaseEntity> {
26
- /** Override specific fields for the next call (reset after consumption). */
27
- 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>;
153
+
154
+ /**
155
+ * Like {@link merge} but deep — nested plain objects are merged key by key
156
+ * instead of replaced wholesale (Lucid `mergeRecursive`). Arrays and
157
+ * non-plain values still replace. Reset after consumption like `merge`.
158
+ */
159
+ mergeRecursive(
160
+ overrides: Partial<Record<string, unknown>>,
161
+ ): FactoryBuilder<T>;
28
162
 
29
- /** Declare a named variation of this factory, stored on the factory's state map. */
30
- 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>;
31
169
 
32
170
  /**
33
171
  * Activate one or more declared states for the NEXT build. Multiple applies
@@ -38,23 +176,146 @@ export interface FactoryBuilder<T extends BaseEntity> {
38
176
  */
39
177
  apply(...stateNames: string[]): FactoryBuilder<T>;
40
178
 
41
- /** Create and persist a single entity (fires lifecycle hooks via `repo.create`). */
42
- create(db: DatabaseConnection): Promise<T>;
179
+ /**
180
+ * Bind a connection (e.g. a transaction) used by subsequent
181
+ * `create`/`createMany` calls that pass no explicit `db` (Adonis Lucid
182
+ * `.client`). The binding persists until changed. Ideal for test isolation:
183
+ * `factory.client(trx).create()`.
184
+ */
185
+ client(connection: DatabaseConnection): FactoryBuilder<T>;
186
+
187
+ /**
188
+ * Like {@link client} but resolves a connection registered under `name` via
189
+ * atlas's connection registry (Adonis Lucid `.connection`). Throws if no
190
+ * connection is registered under that name.
191
+ */
192
+ connection(name: string): FactoryBuilder<T>;
193
+
194
+ /**
195
+ * Create and persist a single entity (fires lifecycle hooks via
196
+ * `repo.create`). `db` is optional when a connection was bound with
197
+ * {@link client} / {@link connection}.
198
+ */
199
+ create(db?: DatabaseConnection): Promise<T>;
43
200
 
44
201
  /** Create and persist multiple entities (fires hooks per row). */
45
- createMany(count: number, db: DatabaseConnection): Promise<T[]>;
202
+ createMany(count: number, db?: DatabaseConnection): Promise<T[]>;
46
203
 
47
- /** Build the data object without persisting and without instantiating an entity. */
48
- 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;
49
212
 
50
- /** Build multiple data objects without persisting. */
51
- makeMany(count: number): Record<string, unknown>[];
213
+ /** Build many un-persisted instances (Adonis Lucid `makeMany`). */
214
+ makeMany(count: number): T[];
52
215
 
53
216
  /**
54
217
  * Build an entity INSTANCE without persisting it (Lucid's `makeStubbed`).
55
- * Useful when you need a `new User()` object but want to avoid the DB.
218
+ * The instance is marked persisted and, unless the build already supplied a
219
+ * primary key, given a process-unique stub id — so it looks like a saved row
220
+ * (relations, serialization, `$isPersisted`) without touching the DB.
56
221
  */
57
222
  makeStubbed(): T;
223
+
224
+ /** Build many stubbed instances, each with its own stub id ({@link makeStubbed}). */
225
+ makeStubbedMany(count: number): T[];
226
+
227
+ /**
228
+ * Declare which factory builds a relation (Adonis Lucid `.relation`), so
229
+ * `.with(name)` can create related rows. `name` must match a relation
230
+ * property declared with `@HasMany`/`@HasOne`/`@BelongsTo`/`@ManyToMany`.
231
+ */
232
+ relation(name: string, resolver: RelationResolver): FactoryBuilder<T>;
233
+
234
+ /**
235
+ * Queue related rows to create together with the next `create`/`createMany`
236
+ * (Adonis Lucid `.with`). `count` defaults to 1 (ignored past 1 for hasOne).
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.
241
+ */
242
+ with(
243
+ name: string,
244
+ count?: number,
245
+ callback?: (factory: FactoryBuilder<BaseEntity>) => void,
246
+ ): FactoryBuilder<T>;
247
+
248
+ /**
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.
310
+ */
311
+ after(
312
+ event: "make" | "create" | "makeStubbed",
313
+ callback: (
314
+ factory: FactoryBuilder<T>,
315
+ model: T,
316
+ ctx: FactoryContext,
317
+ ) => void,
318
+ ): FactoryBuilder<T>;
58
319
  }
59
320
 
60
321
  /**
@@ -65,40 +326,427 @@ export interface FactoryBuilder<T extends BaseEntity> {
65
326
  * name: 'Test User',
66
327
  * }))
67
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
+
68
357
  export function factory<T extends BaseEntity>(
69
358
  entityClass: EntityConstructor<T>,
70
- defaults: () => Record<string, unknown>,
359
+ defaults: DefaultsFn,
71
360
  ): FactoryBuilder<T> {
72
361
  // Persistent state — lives across calls.
73
- const states = new Map<string, StateFn<Record<string, unknown>>>();
362
+ const states = new Map<string, StateFn<T>>();
74
363
  // Transient state — resets after every `make`/`create`.
75
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>[] = [];
370
+ // Recursive overrides are kept apart from the shallow `pendingOverrides`
371
+ // because they must deep-merge into `defaults()`, which only exists at build
372
+ // time — a shallow spread here would clobber a whole nested object.
373
+ let pendingRecursive: Record<string, unknown> = {};
76
374
  let pendingStates: string[] = [];
77
375
 
78
- const buildData = (): Record<string, unknown> => {
79
- const data: Record<string, unknown> = {
80
- ...defaults(),
376
+ // Relations: persistent factory resolvers keyed by relation name, plus the
377
+ // transient `.with()` queue consumed by create()/createMany().
378
+ const relations = new Map<string, RelationResolver>();
379
+ let pendingWith: WithRequest[] = [];
380
+ // Transient `.tap()` callbacks run on the built instance before persistence.
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;
397
+ // Persistent connection bound via `.client()`/`.connection()`, used when
398
+ // create()/createMany() are called without an explicit `db`.
399
+ let boundClient: DatabaseConnection | undefined;
400
+
401
+ /** The connection to persist through: the explicit arg wins over the bound one. */
402
+ const resolveConnection = (db?: DatabaseConnection): DatabaseConnection => {
403
+ const conn = db ?? boundClient;
404
+ if (!conn) {
405
+ throw new Error(
406
+ `Factory ${entityClass.name}: no connection — pass one to create()/createMany() or bind one with .client()/.connection().`,
407
+ );
408
+ }
409
+ return conn;
410
+ };
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
+
430
+ // Primary-key property, resolved once, for stub-id assignment. Same fallback
431
+ // as BaseRepository so a model without an explicit `@PrimaryKey` uses `id`.
432
+ const primaryKey = getPrimaryKey(entityClass) ?? "id";
433
+
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);
440
+ const entity = new entityClass();
441
+ for (const [key, value] of Object.entries(data)) {
442
+ entity.setProp(key, value);
443
+ }
444
+ return entity;
445
+ };
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
+
458
+ /** Build a stubbed instance: hydrate, run taps, give it a stub id if none was
459
+ * supplied, then mark persisted so it mirrors a fetched row. Taps run before
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
+
468
+ const stub = (
469
+ data: Record<string, unknown>,
470
+ taps: Array<
471
+ (model: T, ctx: FactoryContext, builder: FactoryBuilder<T>) => void
472
+ >,
473
+ mergeFns: MergeFn<T>[],
474
+ stateNames: string[],
475
+ ctx: FactoryContext,
476
+ ): T => {
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
+ );
491
+ }
492
+ entity.markAsPersisted();
493
+ for (const hook of afterHooks.makeStubbed) hook(builder, entity, ctx);
494
+ return entity;
495
+ };
496
+
497
+ const buildData = (
498
+ ctx: FactoryContext,
499
+ index = 0,
500
+ ): Record<string, unknown> => {
501
+ let data: Record<string, unknown> = {
502
+ ...defaults(ctx),
81
503
  ...pendingOverrides,
504
+ // Per-row array overrides (merge([...])) win over the shared object.
505
+ ...(pendingOverridesList?.[index] ?? {}),
82
506
  };
83
- for (const name of pendingStates) {
507
+ // Deep overrides layer on top of the shallow one so nested defaults survive.
508
+ if (Object.keys(pendingRecursive).length > 0) {
509
+ data = deepMerge(data, pendingRecursive);
510
+ }
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) {
84
537
  const fn = states.get(name);
85
- if (!fn)
538
+ if (!fn) {
86
539
  throw new Error(
87
540
  `Factory state '${name}' is not defined on ${entityClass.name}Factory`,
88
541
  );
89
- fn(data);
542
+ }
543
+ fn(entity, ctx);
90
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();
91
552
  return data;
92
553
  };
93
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
+
94
566
  const resetPending = (): void => {
95
567
  pendingOverrides = {};
568
+ pendingOverridesList = undefined;
569
+ pendingMergeFns = [];
570
+ pendingRecursive = {};
96
571
  pendingStates = [];
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 = [];
578
+ };
579
+
580
+ /**
581
+ * Persist one row. With no taps this is the plain `repo.create(data)` path,
582
+ * untouched. With taps, the instance is built, tapped, then saved — so a tap
583
+ * can mutate the model before the INSERT (Adonis Lucid `.tap`). `save` on a
584
+ * fresh instance runs the same create hooks as `create`.
585
+ */
586
+ const persist = async (
587
+ repo: BaseRepository<T>,
588
+ data: Record<string, unknown>,
589
+ taps: Array<
590
+ (model: T, ctx: FactoryContext, builder: FactoryBuilder<T>) => void
591
+ >,
592
+ mergeFns: MergeFn<T>[],
593
+ stateNames: string[],
594
+ ctx: FactoryContext,
595
+ ): Promise<T> => {
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);
611
+ await repo.save(entity);
612
+ for (const hook of afterHooks.create) hook(builder, entity, ctx);
613
+ return entity;
614
+ };
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
+
647
+ /**
648
+ * Persist the queued `.with()` relations for one just-created parent, routing
649
+ * through the parent's relation proxy so the FK / pivot is wired by the
650
+ * already-tested relation-write code (not re-derived here).
651
+ */
652
+ const applyRelations = async (
653
+ parent: BaseEntity,
654
+ reqs: WithRequest[],
655
+ db: DatabaseConnection,
656
+ recursive?: Record<string, unknown>,
657
+ ): Promise<void> => {
658
+ const meta = getRelationMetadata(entityClass);
659
+ const hasRecursive = recursive && Object.keys(recursive).length > 0;
660
+ for (const req of reqs) {
661
+ const relMeta = meta.find((r) => r.propertyKey === req.name);
662
+ if (!relMeta) {
663
+ throw new Error(
664
+ `Factory .with('${req.name}'): '${req.name}' is not a declared relation on ${entityClass.name}`,
665
+ );
666
+ }
667
+ const resolver = relations.get(req.name);
668
+ if (!resolver) {
669
+ throw new Error(
670
+ `Factory .with('${req.name}'): no related factory — declare it with .relation('${req.name}', () => XFactory)`,
671
+ );
672
+ }
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);
678
+ if (req.callback) req.callback(childFactory);
679
+ const childInternals = factoryInternals.get(childFactory);
680
+
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
+
706
+ if (proxy.type === "hasMany") {
707
+ await recurse(await proxy.createMany(childFactory.makeMany(req.count)));
708
+ } else if (proxy.type === "hasOne") {
709
+ await recurse([await proxy.create(childFactory.make())]);
710
+ } else if (proxy.type === "manyToMany") {
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)]);
727
+ }
728
+ } else {
729
+ throw new Error(
730
+ `Factory .with('${req.name}'): '${relMeta.type}' relations are not supported`,
731
+ );
732
+ }
733
+ }
97
734
  };
98
735
 
99
736
  const builder: FactoryBuilder<T> = {
100
737
  merge(overrides) {
101
- 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
+ }
745
+ return builder;
746
+ },
747
+
748
+ mergeRecursive(overrides) {
749
+ pendingRecursive = deepMerge(pendingRecursive, overrides);
102
750
  return builder;
103
751
  },
104
752
 
@@ -112,50 +760,199 @@ export function factory<T extends BaseEntity>(
112
760
  return builder;
113
761
  },
114
762
 
763
+ relation(name, resolver) {
764
+ relations.set(name, resolver);
765
+ return builder;
766
+ },
767
+
768
+ with(name, count = 1, callback) {
769
+ pendingWith.push({ name, count, callback });
770
+ return builder;
771
+ },
772
+
773
+ tap(fn) {
774
+ pendingTap.push(fn);
775
+ return builder;
776
+ },
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
+
803
+ client(connection) {
804
+ boundClient = connection;
805
+ return builder;
806
+ },
807
+
808
+ connection(name) {
809
+ const conn = getConnection(name);
810
+ if (!conn) {
811
+ throw new Error(
812
+ `Factory ${entityClass.name}: no connection registered under '${name}'.`,
813
+ );
814
+ }
815
+ boundClient = conn;
816
+ return builder;
817
+ },
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
+
115
834
  make() {
116
- 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);
117
847
  resetPending();
118
- return data;
848
+ return entity;
119
849
  },
120
850
 
121
851
  makeMany(count) {
122
852
  // Re-evaluate defaults for each row so `Date.now()` / faker generate
123
- // distinct values. `buildData()` reads (never mutates) the pending
124
- // overrides/states, so they stay stable across iterations on their own.
125
- 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[] = [];
126
859
  for (let i = 0; i < count; i++) {
127
- 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);
128
867
  }
129
868
  resetPending();
130
869
  return rows;
131
870
  },
132
871
 
133
872
  makeStubbed() {
134
- const data = buildData();
873
+ const taps = pendingTap;
874
+ const mergeFns = pendingMergeFns;
875
+ const stateNames = pendingStates;
876
+ const ctx = makeCtx(true);
877
+ const entity = stub(buildData(ctx), taps, mergeFns, stateNames, ctx);
135
878
  resetPending();
136
- const entity = new entityClass();
137
- for (const [key, value] of Object.entries(data)) {
138
- entity.setProp(key, value);
139
- }
140
879
  return entity;
141
880
  },
142
881
 
882
+ makeStubbedMany(count) {
883
+ // Re-evaluate defaults per row (distinct Date.now()/faker values), same
884
+ // as makeMany; each row then gets its own stub id + taps via `stub`.
885
+ const taps = pendingTap;
886
+ const mergeFns = pendingMergeFns;
887
+ const stateNames = pendingStates;
888
+ const ctx = makeCtx(true);
889
+ const rows: T[] = [];
890
+ for (let i = 0; i < count; i++) {
891
+ rows.push(stub(buildData(ctx, i), taps, mergeFns, stateNames, ctx));
892
+ }
893
+ resetPending();
894
+ return rows;
895
+ },
896
+
143
897
  async create(db) {
144
- const data = builder.make();
145
- const repo = new BaseRepository(entityClass, db);
146
- return repo.create(data);
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().
902
+ const withReqs = pendingWith;
903
+ pendingWith = [];
904
+ const taps = pendingTap;
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
+ );
147
916
  },
148
917
 
149
918
  async createMany(count, db) {
150
- const rows = builder.makeMany(count);
151
- const repo = new BaseRepository(entityClass, db);
919
+ const withReqs = pendingWith;
920
+ pendingWith = [];
921
+ const taps = pendingTap;
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);
152
928
  const created: T[] = [];
153
929
  for (const data of rows) {
154
- created.push(await repo.create(data));
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
+ );
155
936
  }
156
937
  return created;
157
938
  },
158
939
  };
159
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
+
160
957
  return builder;
161
958
  }