@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.
- package/README.md +55 -14
- package/db.darwin-arm64.node +0 -0
- package/db.darwin-x64.node +0 -0
- package/db.linux-arm64-gnu.node +0 -0
- package/db.linux-x64-gnu.node +0 -0
- package/db.win32-x64-msvc.node +0 -0
- package/dist/AtlasProvider.d.ts +6 -0
- package/dist/AtlasProvider.d.ts.map +1 -1
- package/dist/AtlasProvider.js +2 -2
- package/dist/AtlasProvider.js.map +1 -1
- package/dist/BaseEntity.d.ts +171 -7
- package/dist/BaseEntity.d.ts.map +1 -1
- package/dist/BaseEntity.js +339 -31
- package/dist/BaseEntity.js.map +1 -1
- package/dist/BaseModel.d.ts +91 -0
- package/dist/BaseModel.d.ts.map +1 -0
- package/dist/BaseModel.js +193 -0
- package/dist/BaseModel.js.map +1 -0
- package/dist/BaseRepository.d.ts +77 -15
- package/dist/BaseRepository.d.ts.map +1 -1
- package/dist/BaseRepository.js +1423 -354
- package/dist/BaseRepository.js.map +1 -1
- package/dist/ModelQuery.d.ts +429 -11
- package/dist/ModelQuery.d.ts.map +1 -1
- package/dist/ModelQuery.js +1733 -145
- package/dist/ModelQuery.js.map +1 -1
- package/dist/Transaction.d.ts +17 -0
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +57 -5
- package/dist/Transaction.js.map +1 -1
- package/dist/adapters/NapiDbAdapter.d.ts +33 -4
- package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
- package/dist/adapters/NapiDbAdapter.js +101 -11
- package/dist/adapters/NapiDbAdapter.js.map +1 -1
- package/dist/console/migrationCommands.d.ts +48 -0
- package/dist/console/migrationCommands.d.ts.map +1 -0
- package/dist/console/migrationCommands.js +220 -0
- package/dist/console/migrationCommands.js.map +1 -0
- package/dist/decorators/entity.d.ts +37 -6
- package/dist/decorators/entity.d.ts.map +1 -1
- package/dist/decorators/entity.js +32 -2
- package/dist/decorators/entity.js.map +1 -1
- package/dist/events.d.ts +64 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +82 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/metadata-keys.d.ts +3 -2
- package/dist/metadata-keys.d.ts.map +1 -1
- package/dist/naming/NamingStrategy.d.ts +7 -0
- package/dist/naming/NamingStrategy.d.ts.map +1 -1
- package/dist/naming/NamingStrategy.js +16 -0
- package/dist/naming/NamingStrategy.js.map +1 -1
- package/dist/schema/Migration.d.ts +26 -3
- package/dist/schema/Migration.d.ts.map +1 -1
- package/dist/schema/Migration.js +33 -24
- package/dist/schema/Migration.js.map +1 -1
- package/dist/schema/MigrationRunner.d.ts +43 -32
- package/dist/schema/MigrationRunner.d.ts.map +1 -1
- package/dist/schema/MigrationRunner.js +211 -26
- package/dist/schema/MigrationRunner.js.map +1 -1
- package/dist/schema/Schema.d.ts +57 -0
- package/dist/schema/Schema.d.ts.map +1 -1
- package/dist/schema/Schema.js +138 -3
- package/dist/schema/Schema.js.map +1 -1
- package/dist/schema/SchemaCheck.d.ts.map +1 -1
- package/dist/schema/SchemaCheck.js +3 -1
- package/dist/schema/SchemaCheck.js.map +1 -1
- package/dist/schema/TableBuilder.d.ts +247 -8
- package/dist/schema/TableBuilder.d.ts.map +1 -1
- package/dist/schema/TableBuilder.js +607 -41
- package/dist/schema/TableBuilder.js.map +1 -1
- package/dist/schema/catalog.d.ts +47 -0
- package/dist/schema/catalog.d.ts.map +1 -0
- package/dist/schema/catalog.js +111 -0
- package/dist/schema/catalog.js.map +1 -0
- package/dist/schema/introspect.js.map +1 -1
- package/dist/schema/types.d.ts +150 -1
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +11 -0
- package/dist/schema/types.js.map +1 -1
- package/dist/services/db.d.ts +6 -0
- package/dist/services/db.d.ts.map +1 -1
- package/dist/services/db.js +17 -0
- package/dist/services/db.js.map +1 -1
- package/dist/testing/DatabaseCleanup.d.ts +7 -4
- package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
- package/dist/testing/DatabaseCleanup.js +21 -18
- package/dist/testing/DatabaseCleanup.js.map +1 -1
- package/dist/testing/Factory.d.ts +70 -5
- package/dist/testing/Factory.d.ts.map +1 -1
- package/dist/testing/Factory.js +209 -10
- package/dist/testing/Factory.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +4 -1
- package/scripts/guard-publish.mjs +15 -0
- package/src/AtlasProvider.ts +8 -1
- package/src/BaseEntity.ts +449 -40
- package/src/BaseModel.ts +324 -0
- package/src/BaseRepository.ts +1659 -371
- package/src/ModelQuery.ts +2290 -203
- package/src/Transaction.ts +68 -5
- package/src/adapters/NapiDbAdapter.ts +159 -10
- package/src/console/migrationCommands.ts +258 -0
- package/src/decorators/entity.ts +53 -6
- package/src/events.ts +112 -0
- package/src/index.ts +19 -0
- package/src/metadata-keys.ts +3 -2
- package/src/naming/NamingStrategy.ts +23 -0
- package/src/schema/Migration.ts +42 -3
- package/src/schema/MigrationRunner.ts +270 -27
- package/src/schema/Schema.ts +210 -3
- package/src/schema/SchemaCheck.ts +7 -2
- package/src/schema/TableBuilder.ts +735 -41
- package/src/schema/catalog.ts +166 -0
- package/src/schema/introspect.ts +3 -4
- package/src/schema/types.ts +137 -2
- package/src/services/db.ts +28 -0
- package/src/testing/DatabaseCleanup.ts +23 -22
- package/src/testing/Factory.ts +332 -15
package/src/testing/Factory.ts
CHANGED
|
@@ -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
|
-
/**
|
|
42
|
-
|
|
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
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
|
|
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,
|
|
146
|
-
|
|
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,
|
|
465
|
+
const repo = new BaseRepository(entityClass, conn);
|
|
152
466
|
const created: T[] = [];
|
|
153
467
|
for (const data of rows) {
|
|
154
|
-
|
|
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
|
},
|