@c9up/atlas 0.1.18 → 0.2.0

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 (127) hide show
  1. package/README.md +55 -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 +6 -0
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +2 -2
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +171 -7
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js +339 -31
  14. package/dist/BaseEntity.js.map +1 -1
  15. package/dist/BaseModel.d.ts +91 -0
  16. package/dist/BaseModel.d.ts.map +1 -0
  17. package/dist/BaseModel.js +193 -0
  18. package/dist/BaseModel.js.map +1 -0
  19. package/dist/BaseRepository.d.ts +77 -15
  20. package/dist/BaseRepository.d.ts.map +1 -1
  21. package/dist/BaseRepository.js +1423 -354
  22. package/dist/BaseRepository.js.map +1 -1
  23. package/dist/ModelQuery.d.ts +429 -11
  24. package/dist/ModelQuery.d.ts.map +1 -1
  25. package/dist/ModelQuery.js +1733 -145
  26. package/dist/ModelQuery.js.map +1 -1
  27. package/dist/Transaction.d.ts +17 -0
  28. package/dist/Transaction.d.ts.map +1 -1
  29. package/dist/Transaction.js +57 -5
  30. package/dist/Transaction.js.map +1 -1
  31. package/dist/adapters/NapiDbAdapter.d.ts +33 -4
  32. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  33. package/dist/adapters/NapiDbAdapter.js +101 -11
  34. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  35. package/dist/console/migrationCommands.d.ts +48 -0
  36. package/dist/console/migrationCommands.d.ts.map +1 -0
  37. package/dist/console/migrationCommands.js +220 -0
  38. package/dist/console/migrationCommands.js.map +1 -0
  39. package/dist/decorators/entity.d.ts +37 -6
  40. package/dist/decorators/entity.d.ts.map +1 -1
  41. package/dist/decorators/entity.js +32 -2
  42. package/dist/decorators/entity.js.map +1 -1
  43. package/dist/events.d.ts +64 -0
  44. package/dist/events.d.ts.map +1 -0
  45. package/dist/events.js +82 -0
  46. package/dist/events.js.map +1 -0
  47. package/dist/index.d.ts +5 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/metadata-keys.d.ts +3 -2
  52. package/dist/metadata-keys.d.ts.map +1 -1
  53. package/dist/naming/NamingStrategy.d.ts +7 -0
  54. package/dist/naming/NamingStrategy.d.ts.map +1 -1
  55. package/dist/naming/NamingStrategy.js +16 -0
  56. package/dist/naming/NamingStrategy.js.map +1 -1
  57. package/dist/schema/Migration.d.ts +26 -3
  58. package/dist/schema/Migration.d.ts.map +1 -1
  59. package/dist/schema/Migration.js +33 -24
  60. package/dist/schema/Migration.js.map +1 -1
  61. package/dist/schema/MigrationRunner.d.ts +43 -32
  62. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  63. package/dist/schema/MigrationRunner.js +211 -26
  64. package/dist/schema/MigrationRunner.js.map +1 -1
  65. package/dist/schema/Schema.d.ts +57 -0
  66. package/dist/schema/Schema.d.ts.map +1 -1
  67. package/dist/schema/Schema.js +138 -3
  68. package/dist/schema/Schema.js.map +1 -1
  69. package/dist/schema/SchemaCheck.d.ts.map +1 -1
  70. package/dist/schema/SchemaCheck.js +3 -1
  71. package/dist/schema/SchemaCheck.js.map +1 -1
  72. package/dist/schema/TableBuilder.d.ts +247 -8
  73. package/dist/schema/TableBuilder.d.ts.map +1 -1
  74. package/dist/schema/TableBuilder.js +607 -41
  75. package/dist/schema/TableBuilder.js.map +1 -1
  76. package/dist/schema/catalog.d.ts +47 -0
  77. package/dist/schema/catalog.d.ts.map +1 -0
  78. package/dist/schema/catalog.js +111 -0
  79. package/dist/schema/catalog.js.map +1 -0
  80. package/dist/schema/introspect.js.map +1 -1
  81. package/dist/schema/types.d.ts +150 -1
  82. package/dist/schema/types.d.ts.map +1 -1
  83. package/dist/schema/types.js +11 -0
  84. package/dist/schema/types.js.map +1 -1
  85. package/dist/services/db.d.ts +6 -0
  86. package/dist/services/db.d.ts.map +1 -1
  87. package/dist/services/db.js +17 -0
  88. package/dist/services/db.js.map +1 -1
  89. package/dist/testing/DatabaseCleanup.d.ts +7 -4
  90. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  91. package/dist/testing/DatabaseCleanup.js +21 -18
  92. package/dist/testing/DatabaseCleanup.js.map +1 -1
  93. package/dist/testing/Factory.d.ts +70 -5
  94. package/dist/testing/Factory.d.ts.map +1 -1
  95. package/dist/testing/Factory.js +209 -10
  96. package/dist/testing/Factory.js.map +1 -1
  97. package/index.darwin-arm64.node +0 -0
  98. package/index.darwin-x64.node +0 -0
  99. package/index.linux-arm64-gnu.node +0 -0
  100. package/index.linux-x64-gnu.node +0 -0
  101. package/index.win32-x64-msvc.node +0 -0
  102. package/package.json +4 -1
  103. package/scripts/guard-publish.mjs +15 -0
  104. package/src/AtlasProvider.ts +8 -1
  105. package/src/BaseEntity.ts +449 -40
  106. package/src/BaseModel.ts +324 -0
  107. package/src/BaseRepository.ts +1659 -371
  108. package/src/ModelQuery.ts +2290 -203
  109. package/src/Transaction.ts +68 -5
  110. package/src/adapters/NapiDbAdapter.ts +159 -10
  111. package/src/console/migrationCommands.ts +258 -0
  112. package/src/decorators/entity.ts +53 -6
  113. package/src/events.ts +112 -0
  114. package/src/index.ts +19 -0
  115. package/src/metadata-keys.ts +3 -2
  116. package/src/naming/NamingStrategy.ts +23 -0
  117. package/src/schema/Migration.ts +42 -3
  118. package/src/schema/MigrationRunner.ts +270 -27
  119. package/src/schema/Schema.ts +210 -3
  120. package/src/schema/SchemaCheck.ts +7 -2
  121. package/src/schema/TableBuilder.ts +735 -41
  122. package/src/schema/catalog.ts +166 -0
  123. package/src/schema/introspect.ts +3 -4
  124. package/src/schema/types.ts +137 -2
  125. package/src/services/db.ts +28 -0
  126. package/src/testing/DatabaseCleanup.ts +23 -22
  127. package/src/testing/Factory.ts +332 -15
@@ -14,18 +14,90 @@
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";
19
22
 
20
23
  type EntityConstructor<T extends BaseEntity> = new () => T;
21
24
 
25
+ /** Resolves the factory that builds a related model (Lucid `.relation`). */
26
+ type RelationResolver = () => FactoryBuilder<BaseEntity>;
27
+
28
+ /** A queued `.with()` request, applied on the next create()/createMany(). */
29
+ interface WithRequest {
30
+ name: string;
31
+ count: number;
32
+ callback?: (factory: FactoryBuilder<BaseEntity>) => void;
33
+ }
34
+
35
+ /**
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.
39
+ */
40
+ export interface FactoryContext {
41
+ faker: Faker;
42
+ }
43
+
44
+ /** Attributes callback — receives {@link FactoryContext}, returns the row shape. */
45
+ type DefaultsFn = (ctx: FactoryContext) => Record<string, unknown>;
46
+
22
47
  /** A named state — mutates an in-progress data object in place. */
23
48
  type StateFn<D> = (data: D) => void;
24
49
 
50
+ /**
51
+ * Process-wide counter for stubbed primary keys (Lucid's stub id). Every
52
+ * `makeStubbed*` build with no explicit PK gets the next value, so stubbed
53
+ * instances have stable, distinct, DB-free identifiers — enough to look
54
+ * persisted and to key relations/serialization without a round trip.
55
+ */
56
+ let stubIdCounter = 0;
57
+
58
+ /** Keys that must never be written through a recursive merge — prototype-pollution guard. */
59
+ const FORBIDDEN_MERGE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
60
+
61
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
62
+ if (value === null || typeof value !== "object") return false;
63
+ const proto = Object.getPrototypeOf(value);
64
+ return proto === Object.prototype || proto === null;
65
+ }
66
+
67
+ /**
68
+ * Deep-merge `source` into `target` (Lucid `mergeRecursive`). Plain objects are
69
+ * merged key by key; arrays and every non-plain value replace wholesale. Returns
70
+ * a fresh object — neither argument is mutated. Skips prototype-pollution keys.
71
+ */
72
+ function deepMerge(
73
+ target: Record<string, unknown>,
74
+ source: Record<string, unknown>,
75
+ ): Record<string, unknown> {
76
+ const out: Record<string, unknown> = { ...target };
77
+ for (const [key, value] of Object.entries(source)) {
78
+ if (FORBIDDEN_MERGE_KEYS.has(key)) continue;
79
+ const existing = out[key];
80
+ out[key] =
81
+ isPlainObject(existing) && isPlainObject(value)
82
+ ? deepMerge(existing, value)
83
+ : value;
84
+ }
85
+ return out;
86
+ }
87
+
25
88
  export interface FactoryBuilder<T extends BaseEntity> {
26
89
  /** Override specific fields for the next call (reset after consumption). */
27
90
  merge(overrides: Partial<Record<string, unknown>>): FactoryBuilder<T>;
28
91
 
92
+ /**
93
+ * Like {@link merge} but deep — nested plain objects are merged key by key
94
+ * instead of replaced wholesale (Lucid `mergeRecursive`). Arrays and
95
+ * non-plain values still replace. Reset after consumption like `merge`.
96
+ */
97
+ mergeRecursive(
98
+ overrides: Partial<Record<string, unknown>>,
99
+ ): FactoryBuilder<T>;
100
+
29
101
  /** Declare a named variation of this factory, stored on the factory's state map. */
30
102
  state(name: string, fn: StateFn<Record<string, unknown>>): FactoryBuilder<T>;
31
103
 
@@ -38,11 +110,30 @@ export interface FactoryBuilder<T extends BaseEntity> {
38
110
  */
39
111
  apply(...stateNames: string[]): FactoryBuilder<T>;
40
112
 
41
- /** Create and persist a single entity (fires lifecycle hooks via `repo.create`). */
42
- create(db: DatabaseConnection): Promise<T>;
113
+ /**
114
+ * Bind a connection (e.g. a transaction) used by subsequent
115
+ * `create`/`createMany` calls that pass no explicit `db` (Adonis Lucid
116
+ * `.client`). The binding persists until changed. Ideal for test isolation:
117
+ * `factory.client(trx).create()`.
118
+ */
119
+ client(connection: DatabaseConnection): FactoryBuilder<T>;
120
+
121
+ /**
122
+ * Like {@link client} but resolves a connection registered under `name` via
123
+ * atlas's connection registry (Adonis Lucid `.connection`). Throws if no
124
+ * connection is registered under that name.
125
+ */
126
+ connection(name: string): FactoryBuilder<T>;
127
+
128
+ /**
129
+ * Create and persist a single entity (fires lifecycle hooks via
130
+ * `repo.create`). `db` is optional when a connection was bound with
131
+ * {@link client} / {@link connection}.
132
+ */
133
+ create(db?: DatabaseConnection): Promise<T>;
43
134
 
44
135
  /** Create and persist multiple entities (fires hooks per row). */
45
- createMany(count: number, db: DatabaseConnection): Promise<T[]>;
136
+ createMany(count: number, db?: DatabaseConnection): Promise<T[]>;
46
137
 
47
138
  /** Build the data object without persisting and without instantiating an entity. */
48
139
  make(): Record<string, unknown>;
@@ -52,9 +143,46 @@ export interface FactoryBuilder<T extends BaseEntity> {
52
143
 
53
144
  /**
54
145
  * 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.
146
+ * The instance is marked persisted and, unless the build already supplied a
147
+ * primary key, given a process-unique stub id — so it looks like a saved row
148
+ * (relations, serialization, `$isPersisted`) without touching the DB.
56
149
  */
57
150
  makeStubbed(): T;
151
+
152
+ /** Build many stubbed instances, each with its own stub id ({@link makeStubbed}). */
153
+ makeStubbedMany(count: number): T[];
154
+
155
+ /**
156
+ * Declare which factory builds a relation (Adonis Lucid `.relation`), so
157
+ * `.with(name)` can create related rows. `name` must match a relation
158
+ * property declared with `@HasMany`/`@HasOne`/`@BelongsTo`/`@ManyToMany`.
159
+ */
160
+ relation(name: string, resolver: RelationResolver): FactoryBuilder<T>;
161
+
162
+ /**
163
+ * Queue related rows to create together with the next `create`/`createMany`
164
+ * (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.
171
+ */
172
+ with(
173
+ name: string,
174
+ count?: number,
175
+ callback?: (factory: FactoryBuilder<BaseEntity>) => void,
176
+ ): FactoryBuilder<T>;
177
+
178
+ /**
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.
184
+ */
185
+ tap(fn: (entity: T) => void): FactoryBuilder<T>;
58
186
  }
59
187
 
60
188
  /**
@@ -67,19 +195,77 @@ export interface FactoryBuilder<T extends BaseEntity> {
67
195
  */
68
196
  export function factory<T extends BaseEntity>(
69
197
  entityClass: EntityConstructor<T>,
70
- defaults: () => Record<string, unknown>,
198
+ defaults: DefaultsFn,
71
199
  ): FactoryBuilder<T> {
72
200
  // Persistent state — lives across calls.
73
201
  const states = new Map<string, StateFn<Record<string, unknown>>>();
74
202
  // Transient state — resets after every `make`/`create`.
75
203
  let pendingOverrides: Partial<Record<string, unknown>> = {};
204
+ // Recursive overrides are kept apart from the shallow `pendingOverrides`
205
+ // because they must deep-merge into `defaults()`, which only exists at build
206
+ // time — a shallow spread here would clobber a whole nested object.
207
+ let pendingRecursive: Record<string, unknown> = {};
76
208
  let pendingStates: string[] = [];
77
209
 
210
+ // Relations: persistent factory resolvers keyed by relation name, plus the
211
+ // transient `.with()` queue consumed by create()/createMany().
212
+ const relations = new Map<string, RelationResolver>();
213
+ let pendingWith: WithRequest[] = [];
214
+ // Transient `.tap()` callbacks run on the built instance before persistence.
215
+ let pendingTap: Array<(entity: T) => void> = [];
216
+ // Persistent connection bound via `.client()`/`.connection()`, used when
217
+ // create()/createMany() are called without an explicit `db`.
218
+ let boundClient: DatabaseConnection | undefined;
219
+
220
+ /** The connection to persist through: the explicit arg wins over the bound one. */
221
+ const resolveConnection = (db?: DatabaseConnection): DatabaseConnection => {
222
+ const conn = db ?? boundClient;
223
+ if (!conn) {
224
+ throw new Error(
225
+ `Factory ${entityClass.name}: no connection — pass one to create()/createMany() or bind one with .client()/.connection().`,
226
+ );
227
+ }
228
+ return conn;
229
+ };
230
+
231
+ // Primary-key property, resolved once, for stub-id assignment. Same fallback
232
+ // as BaseRepository so a model without an explicit `@PrimaryKey` uses `id`.
233
+ const primaryKey = getPrimaryKey(entityClass) ?? "id";
234
+
235
+ /** Hydrate a fresh (not-yet-persisted) instance from a data object. */
236
+ const newInstance = (data: Record<string, unknown>): T => {
237
+ const entity = new entityClass();
238
+ for (const [key, value] of Object.entries(data)) {
239
+ entity.setProp(key, value);
240
+ }
241
+ return entity;
242
+ };
243
+
244
+ /** Build a stubbed instance: hydrate, run taps, give it a stub id if none was
245
+ * supplied, then mark persisted so it mirrors a fetched row. Taps run before
246
+ * `markAsPersisted` so tapped fields land in the clean snapshot. */
247
+ const stub = (
248
+ data: Record<string, unknown>,
249
+ taps: Array<(entity: T) => void>,
250
+ ): T => {
251
+ const entity = newInstance(data);
252
+ for (const tap of taps) tap(entity);
253
+ if (data[primaryKey] === undefined) {
254
+ entity.setProp(primaryKey, ++stubIdCounter);
255
+ }
256
+ entity.markAsPersisted();
257
+ return entity;
258
+ };
259
+
78
260
  const buildData = (): Record<string, unknown> => {
79
- const data: Record<string, unknown> = {
80
- ...defaults(),
261
+ let data: Record<string, unknown> = {
262
+ ...defaults({ faker }),
81
263
  ...pendingOverrides,
82
264
  };
265
+ // Deep overrides layer on top of the shallow one so nested defaults survive.
266
+ if (Object.keys(pendingRecursive).length > 0) {
267
+ data = deepMerge(data, pendingRecursive);
268
+ }
83
269
  for (const name of pendingStates) {
84
270
  const fn = states.get(name);
85
271
  if (!fn)
@@ -93,7 +279,77 @@ export function factory<T extends BaseEntity>(
93
279
 
94
280
  const resetPending = (): void => {
95
281
  pendingOverrides = {};
282
+ pendingRecursive = {};
96
283
  pendingStates = [];
284
+ pendingTap = [];
285
+ };
286
+
287
+ /**
288
+ * Persist one row. With no taps this is the plain `repo.create(data)` path,
289
+ * untouched. With taps, the instance is built, tapped, then saved — so a tap
290
+ * can mutate the model before the INSERT (Adonis Lucid `.tap`). `save` on a
291
+ * fresh instance runs the same create hooks as `create`.
292
+ */
293
+ const persist = async (
294
+ repo: BaseRepository<T>,
295
+ data: Record<string, unknown>,
296
+ taps: Array<(entity: T) => void>,
297
+ ): Promise<T> => {
298
+ if (taps.length === 0) return repo.create(data);
299
+ const entity = newInstance(data);
300
+ for (const tap of taps) tap(entity);
301
+ await repo.save(entity);
302
+ return entity;
303
+ };
304
+
305
+ /**
306
+ * Persist the queued `.with()` relations for one just-created parent, routing
307
+ * through the parent's relation proxy so the FK / pivot is wired by the
308
+ * already-tested relation-write code (not re-derived here).
309
+ */
310
+ const applyRelations = async (
311
+ parent: T,
312
+ reqs: WithRequest[],
313
+ db: DatabaseConnection,
314
+ ): Promise<void> => {
315
+ const meta = getRelationMetadata(entityClass);
316
+ for (const req of reqs) {
317
+ const relMeta = meta.find((r) => r.propertyKey === req.name);
318
+ if (!relMeta) {
319
+ throw new Error(
320
+ `Factory .with('${req.name}'): '${req.name}' is not a declared relation on ${entityClass.name}`,
321
+ );
322
+ }
323
+ const resolver = relations.get(req.name);
324
+ if (!resolver) {
325
+ throw new Error(
326
+ `Factory .with('${req.name}'): no related factory — declare it with .relation('${req.name}', () => XFactory)`,
327
+ );
328
+ }
329
+ const childFactory = resolver();
330
+ if (req.callback) req.callback(childFactory);
331
+
332
+ const proxy = parent.related(req.name);
333
+ if (proxy.type === "hasMany") {
334
+ await proxy.createMany(childFactory.makeMany(req.count));
335
+ } else if (proxy.type === "hasOne") {
336
+ await proxy.create(childFactory.make());
337
+ } 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);
341
+ }
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
+ } else {
348
+ throw new Error(
349
+ `Factory .with('${req.name}'): '${relMeta.type}' relations are not supported`,
350
+ );
351
+ }
352
+ }
97
353
  };
98
354
 
99
355
  const builder: FactoryBuilder<T> = {
@@ -102,6 +358,11 @@ export function factory<T extends BaseEntity>(
102
358
  return builder;
103
359
  },
104
360
 
361
+ mergeRecursive(overrides) {
362
+ pendingRecursive = deepMerge(pendingRecursive, overrides);
363
+ return builder;
364
+ },
365
+
105
366
  state(name, fn) {
106
367
  states.set(name, fn);
107
368
  return builder;
@@ -112,6 +373,37 @@ export function factory<T extends BaseEntity>(
112
373
  return builder;
113
374
  },
114
375
 
376
+ relation(name, resolver) {
377
+ relations.set(name, resolver);
378
+ return builder;
379
+ },
380
+
381
+ with(name, count = 1, callback) {
382
+ pendingWith.push({ name, count, callback });
383
+ return builder;
384
+ },
385
+
386
+ tap(fn) {
387
+ pendingTap.push(fn);
388
+ return builder;
389
+ },
390
+
391
+ client(connection) {
392
+ boundClient = connection;
393
+ return builder;
394
+ },
395
+
396
+ connection(name) {
397
+ const conn = getConnection(name);
398
+ if (!conn) {
399
+ throw new Error(
400
+ `Factory ${entityClass.name}: no connection registered under '${name}'.`,
401
+ );
402
+ }
403
+ boundClient = conn;
404
+ return builder;
405
+ },
406
+
115
407
  make() {
116
408
  const data = buildData();
117
409
  resetPending();
@@ -131,27 +423,52 @@ export function factory<T extends BaseEntity>(
131
423
  },
132
424
 
133
425
  makeStubbed() {
426
+ const taps = pendingTap;
134
427
  const data = buildData();
428
+ const entity = stub(data, taps);
135
429
  resetPending();
136
- const entity = new entityClass();
137
- for (const [key, value] of Object.entries(data)) {
138
- entity.setProp(key, value);
139
- }
140
430
  return entity;
141
431
  },
142
432
 
433
+ makeStubbedMany(count) {
434
+ // Re-evaluate defaults per row (distinct Date.now()/faker values), same
435
+ // as makeMany; each row then gets its own stub id + taps via `stub`.
436
+ const taps = pendingTap;
437
+ const rows: T[] = [];
438
+ for (let i = 0; i < count; i++) {
439
+ rows.push(stub(buildData(), taps));
440
+ }
441
+ resetPending();
442
+ return rows;
443
+ },
444
+
143
445
  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.
449
+ const withReqs = pendingWith;
450
+ pendingWith = [];
451
+ const taps = pendingTap;
144
452
  const data = builder.make();
145
- const repo = new BaseRepository(entityClass, db);
146
- return repo.create(data);
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;
147
457
  },
148
458
 
149
459
  async createMany(count, db) {
460
+ const conn = resolveConnection(db);
461
+ const withReqs = pendingWith;
462
+ pendingWith = [];
463
+ const taps = pendingTap;
150
464
  const rows = builder.makeMany(count);
151
- const repo = new BaseRepository(entityClass, db);
465
+ const repo = new BaseRepository(entityClass, conn);
152
466
  const created: T[] = [];
153
467
  for (const data of rows) {
154
- created.push(await repo.create(data));
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);
155
472
  }
156
473
  return created;
157
474
  },