@aklinker1/zero-factory 1.0.2 → 1.1.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 +66 -32
- package/dist/factories.d.ts +71 -5
- package/dist/index.js +50 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,45 @@ const user = userFactory({
|
|
|
98
98
|
> [!IMPORTANT]
|
|
99
99
|
> Arrays are not deeply merged. If a property is an array, overrides will fully replace it, like any other value.
|
|
100
100
|
|
|
101
|
+
#### Function Defaults
|
|
102
|
+
|
|
103
|
+
In addition to static values, you can pass a function as a value:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const userFactory = createFactory({
|
|
107
|
+
email: () => `example.${Math.floor(Math.random() * 1000)}@gmail.com`,
|
|
108
|
+
// ...
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Every time the factory is called, this will call the function and, in this case, generate a different `email` each time:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
userFactory(); // { email: "example.424@gmail.com", ... }
|
|
116
|
+
userFactory(); // { email: "example.133@gmail.com", ... }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This is where [fake data generators](https://www.npmjs.com/search?q=fake%20data) and [sequences](#sequences) come in clutch:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { createFactory, createSequence } from "@aklinker1/zero-factory";
|
|
123
|
+
import {
|
|
124
|
+
randEmail, // () => string
|
|
125
|
+
randUsername, // () => string
|
|
126
|
+
randBoolean, // () => boolean
|
|
127
|
+
} from "@ngneat/falso";
|
|
128
|
+
|
|
129
|
+
const userFactory = createFactory({
|
|
130
|
+
id: createSequence("user-"),
|
|
131
|
+
username: randUsername,
|
|
132
|
+
email: randEmail,
|
|
133
|
+
preferences: {
|
|
134
|
+
receiveMarketingEmails: randBoolean,
|
|
135
|
+
receiveSecurityEmails: randBoolean,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
101
140
|
#### Many
|
|
102
141
|
|
|
103
142
|
You can generate multiple objects using `factory.many(...)`. This method will return an array of objects.
|
|
@@ -157,43 +196,39 @@ const user = userFactory.noEmails({ username: "overridden" });
|
|
|
157
196
|
// }
|
|
158
197
|
```
|
|
159
198
|
|
|
160
|
-
|
|
199
|
+
#### Associations
|
|
161
200
|
|
|
162
|
-
|
|
201
|
+
If you want to override one or more fields based on a single value, use associations:
|
|
163
202
|
|
|
164
203
|
```ts
|
|
165
|
-
const
|
|
166
|
-
|
|
204
|
+
const postFactory = createFactory<Post>({
|
|
205
|
+
id: createSequence(),
|
|
206
|
+
userId: userIdSequence,
|
|
167
207
|
// ...
|
|
168
|
-
});
|
|
208
|
+
}).associate("user", (user: User) => ({ userId: user.id }));
|
|
169
209
|
```
|
|
170
210
|
|
|
171
|
-
|
|
211
|
+
Then to generate a post associated with a user, use `with`:
|
|
172
212
|
|
|
173
213
|
```ts
|
|
174
|
-
|
|
175
|
-
|
|
214
|
+
user;
|
|
215
|
+
// => {
|
|
216
|
+
// id: 3,
|
|
217
|
+
// ...
|
|
218
|
+
// }
|
|
219
|
+
|
|
220
|
+
postFactory.with({ user })();
|
|
221
|
+
// => {
|
|
222
|
+
// id: 0,
|
|
223
|
+
// userId: 3,
|
|
224
|
+
// ...
|
|
225
|
+
// }
|
|
176
226
|
```
|
|
177
227
|
|
|
178
|
-
|
|
228
|
+
Note that `with` returns a factory function, which needs to be called to generate the final object. This allows you to chain other utilities like `.many` or `.trait`:
|
|
179
229
|
|
|
180
230
|
```ts
|
|
181
|
-
|
|
182
|
-
import {
|
|
183
|
-
randEmail, // () => string
|
|
184
|
-
randUsername, // () => string
|
|
185
|
-
randBoolean, // () => boolean
|
|
186
|
-
} from "@ngneat/falso";
|
|
187
|
-
|
|
188
|
-
const userFactory = createFactory({
|
|
189
|
-
id: createSequence("user-"),
|
|
190
|
-
username: randUsername,
|
|
191
|
-
email: randEmail,
|
|
192
|
-
preferences: {
|
|
193
|
-
receiveMarketingEmails: randBoolean,
|
|
194
|
-
receiveSecurityEmails: randBoolean,
|
|
195
|
-
},
|
|
196
|
-
});
|
|
231
|
+
postFactory.with({ user }).noEmails.many(3);
|
|
197
232
|
```
|
|
198
233
|
|
|
199
234
|
### Sequences
|
|
@@ -249,19 +284,18 @@ May or may not implement these.
|
|
|
249
284
|
- Associations:
|
|
250
285
|
|
|
251
286
|
```ts
|
|
252
|
-
const userIdSequence = createSequence("user-")
|
|
287
|
+
const userIdSequence = createSequence("user-");
|
|
253
288
|
const userFactory = createFactory<User>({
|
|
254
289
|
id: userIdSequence,
|
|
255
290
|
// ...
|
|
256
|
-
})
|
|
291
|
+
});
|
|
257
292
|
const postFactory = createFactory<Post>({
|
|
258
293
|
id: createSequence("post-"),
|
|
259
294
|
userId: userIdSequence,
|
|
260
|
-
})
|
|
261
|
-
.
|
|
262
|
-
|
|
263
|
-
}))
|
|
295
|
+
}).associate("user", (user: User) => ({
|
|
296
|
+
userId: user.id,
|
|
297
|
+
}));
|
|
264
298
|
|
|
265
299
|
const user = userFactory(); // { id: "user-0", ... }
|
|
266
|
-
|
|
300
|
+
postFactory.with({ user })(/* optional overrides */); // { id: "post-0", userId: "user-0", ... }
|
|
267
301
|
```
|
package/dist/factories.d.ts
CHANGED
|
@@ -5,11 +5,11 @@ import { type DeepPartial, type FactoryDefaults } from "./utils";
|
|
|
5
5
|
* - Object containing any `.traitName(...)` functions that, when called, return a new object applying the trait's defaults over the factory's base defaults.
|
|
6
6
|
* - Object containing immutable modifier functions (`trait`) that, when called, returns a new factory with more ways of generating objects.
|
|
7
7
|
*/
|
|
8
|
-
export type Factory<TObject extends Record<string, any>, TTraits extends string | undefined = undefined> = FactoryFn<TObject> & TraitFactoryFns<TObject, TTraits extends string ? TTraits : never> & FactoryModifiers<TObject, TTraits>;
|
|
8
|
+
export type Factory<TObject extends Record<string, any>, TTraits extends string | undefined = undefined, TAssociations extends Record<string, any> = {}> = FactoryFn<TObject, TAssociations> & TraitFactoryFns<TObject, TTraits extends string ? TTraits : never, TAssociations> & FactoryModifiers<TObject, TTraits>;
|
|
9
9
|
/**
|
|
10
10
|
* Function that takes in overrides and returns a new object.
|
|
11
11
|
*/
|
|
12
|
-
export type FactoryFn<TObject> = {
|
|
12
|
+
export type FactoryFn<TObject, TAssociations extends Record<string, any> = {}> = {
|
|
13
13
|
(overrides?: DeepPartial<TObject>): TObject;
|
|
14
14
|
/**
|
|
15
15
|
* Generate multiple items.
|
|
@@ -26,17 +26,23 @@ export type FactoryFn<TObject> = {
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
many(count: number, overrides?: DeepPartial<TObject>): TObject[];
|
|
29
|
+
/**
|
|
30
|
+
* Apply associations and return a new factory function.
|
|
31
|
+
*
|
|
32
|
+
* @see {@link FactoryModifiers#associate}
|
|
33
|
+
*/
|
|
34
|
+
with(associations: Partial<TAssociations>): FactoryFn<TObject, {}>;
|
|
29
35
|
};
|
|
30
36
|
/**
|
|
31
37
|
* Map of factory functions for traits.
|
|
32
38
|
*/
|
|
33
|
-
export type TraitFactoryFns<TObject, TTraits extends string> = {
|
|
34
|
-
[name in TTraits]: FactoryFn<TObject>;
|
|
39
|
+
export type TraitFactoryFns<TObject, TTraits extends string, TAssociations extends Record<string, any> = {}> = {
|
|
40
|
+
[name in TTraits]: FactoryFn<TObject, TAssociations>;
|
|
35
41
|
};
|
|
36
42
|
/**
|
|
37
43
|
* Functions that modify the factory's type.
|
|
38
44
|
*/
|
|
39
|
-
export type FactoryModifiers<TObject extends Record<string, any>, TTraits extends string | undefined> = {
|
|
45
|
+
export type FactoryModifiers<TObject extends Record<string, any>, TTraits extends string | undefined, TAssociations extends Record<string, any> = {}> = {
|
|
40
46
|
/**
|
|
41
47
|
* Add a trait or variant to the factory, allowing developers to create the
|
|
42
48
|
* object with multiple sets of default values.
|
|
@@ -59,8 +65,68 @@ export type FactoryModifiers<TObject extends Record<string, any>, TTraits extend
|
|
|
59
65
|
* calling the factory function.
|
|
60
66
|
*/
|
|
61
67
|
trait<T2 extends string>(name: T2, traitDefaults: DeepPartial<FactoryDefaults<TObject>>): Factory<TObject, AddTrait<TTraits, T2>>;
|
|
68
|
+
/**
|
|
69
|
+
* Returns a factory that uses associations to apply default values.
|
|
70
|
+
*
|
|
71
|
+
* There are two common use-cases:
|
|
72
|
+
* - Generating "dependent" properties
|
|
73
|
+
* - Database relationships
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const userFactory = createFactory<User>({
|
|
78
|
+
* id: createSequence("user-"),
|
|
79
|
+
* email: randEmail(),
|
|
80
|
+
* fullName: randFullName(),
|
|
81
|
+
* firstName: randFirstName(),
|
|
82
|
+
* lastName: randLastName(),
|
|
83
|
+
* })
|
|
84
|
+
* .associate("fullName", (fullName: string) => ({
|
|
85
|
+
* fullName,
|
|
86
|
+
* firstName: fullName.split(" ")[0],
|
|
87
|
+
* lastName: fullName.split(" ")[1],
|
|
88
|
+
* })
|
|
89
|
+
*
|
|
90
|
+
* userFactory.with({ fullName: "John Doe" })()
|
|
91
|
+
* // {
|
|
92
|
+
* // id: "user-0",
|
|
93
|
+
* // email: "john.doe@example.com",
|
|
94
|
+
* // fullName: "John Doe",
|
|
95
|
+
* // firstName: "John",
|
|
96
|
+
* // lastName: "Doe",
|
|
97
|
+
* // }
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* const postFactory = createFactory<Post>({
|
|
103
|
+
* id: createSequence("post-"),
|
|
104
|
+
* userId: createSequence("user-"),
|
|
105
|
+
* // ...
|
|
106
|
+
* })
|
|
107
|
+
* .associate("user", (user: User) => ({
|
|
108
|
+
* userId: user.id,
|
|
109
|
+
* })
|
|
110
|
+
*
|
|
111
|
+
* const user = userFactory();
|
|
112
|
+
* // {
|
|
113
|
+
* // id: "user-0",
|
|
114
|
+
* // ...
|
|
115
|
+
* // }
|
|
116
|
+
* const post = postFactory.with({ user })()
|
|
117
|
+
* // {
|
|
118
|
+
* // id: "post-0",
|
|
119
|
+
* // userId: "user-0",
|
|
120
|
+
* // // ...
|
|
121
|
+
* // }
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
associate<TKey extends string, TValue>(key: TKey, apply: (value: TValue) => DeepPartial<TObject>): Factory<TObject, TTraits, AddAssociation<TAssociations, TKey, TValue>>;
|
|
62
125
|
};
|
|
63
126
|
export type AddTrait<T1 extends string | undefined, T2 extends string> = T1 extends string ? T1 | T2 : T2;
|
|
127
|
+
export type AddAssociation<TAssociations extends Record<string, any>, TKey extends string, TValue> = {
|
|
128
|
+
[key in keyof TAssociations | TKey]: key extends TKey ? TValue : TAssociations[key];
|
|
129
|
+
};
|
|
64
130
|
/**
|
|
65
131
|
* Create a function that returns objects of the specified type.
|
|
66
132
|
* @param defaults The default values for the returned object. Each property can be a value or function that return a value.
|
package/dist/index.js
CHANGED
|
@@ -10,14 +10,16 @@ function createSequence(arg) {
|
|
|
10
10
|
// src/utils.ts
|
|
11
11
|
function deepMerge(base, overrides) {
|
|
12
12
|
if (!isMergeable(overrides))
|
|
13
|
-
return
|
|
14
|
-
return Object.fromEntries(Object.keys({ ...base, ...overrides }).map((key) =>
|
|
15
|
-
key
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
return overrides ?? base;
|
|
14
|
+
return Object.fromEntries(Object.keys({ ...base, ...overrides }).map((key) => {
|
|
15
|
+
const baseValue = base[key];
|
|
16
|
+
if (!(key in overrides))
|
|
17
|
+
return [key, baseValue];
|
|
18
|
+
const overrideValue = overrides[key];
|
|
19
|
+
if (isMergeable(overrideValue))
|
|
20
|
+
return [key, deepMerge(baseValue, overrideValue)];
|
|
21
|
+
return [key, overrideValue];
|
|
22
|
+
}).filter((entry) => entry != null));
|
|
21
23
|
}
|
|
22
24
|
function isMergeable(val) {
|
|
23
25
|
return val != null && typeof val === "object" && !Array.isArray(val) && !(val instanceof Date);
|
|
@@ -41,20 +43,49 @@ function resolveDefaults(val) {
|
|
|
41
43
|
|
|
42
44
|
// src/factories.ts
|
|
43
45
|
function createFactory(defaults) {
|
|
44
|
-
return createFactoryInternal(defaults, {
|
|
46
|
+
return createFactoryInternal(defaults, {
|
|
47
|
+
traits: {},
|
|
48
|
+
associations: {}
|
|
49
|
+
});
|
|
45
50
|
}
|
|
46
|
-
function createFactoryInternal(defaults,
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
function createFactoryInternal(defaults, state) {
|
|
52
|
+
const createFactoryFn = (factoryDefaults) => {
|
|
53
|
+
const factoryFn = (overrides) => generateObject(factoryDefaults, overrides);
|
|
54
|
+
factoryFn.many = (count, overrides) => generateManyObjects(count, factoryDefaults, overrides);
|
|
55
|
+
factoryFn.with = (associations) => {
|
|
56
|
+
const combinedDefaults = Object.entries(associations).reduce((acc, [key, value]) => {
|
|
57
|
+
if (state.associations[key]) {
|
|
58
|
+
const override = state.associations[key](value);
|
|
59
|
+
return deepMerge(acc, override);
|
|
60
|
+
} else {
|
|
61
|
+
return acc;
|
|
62
|
+
}
|
|
63
|
+
}, defaults);
|
|
64
|
+
return createFactoryInternal(combinedDefaults, state);
|
|
65
|
+
};
|
|
66
|
+
return factoryFn;
|
|
67
|
+
};
|
|
68
|
+
return Object.assign(createFactoryFn(defaults), {
|
|
49
69
|
trait: (name, traitDefaults) => createFactoryInternal(defaults, {
|
|
50
|
-
...
|
|
51
|
-
|
|
70
|
+
...state,
|
|
71
|
+
traits: {
|
|
72
|
+
...state.traits,
|
|
73
|
+
[name]: deepMerge(defaults, traitDefaults)
|
|
74
|
+
}
|
|
52
75
|
}),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
associate: (key, apply) => {
|
|
77
|
+
return createFactoryInternal(defaults, {
|
|
78
|
+
...state,
|
|
79
|
+
associations: {
|
|
80
|
+
...state.associations,
|
|
81
|
+
[key]: apply
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
...Object.fromEntries(Object.entries(state.traits).map(([name, traitDefaults]) => [
|
|
86
|
+
name,
|
|
87
|
+
createFactoryFn(traitDefaults)
|
|
88
|
+
]))
|
|
58
89
|
});
|
|
59
90
|
}
|
|
60
91
|
function generateObject(defaults, overrides) {
|