@aklinker1/zero-factory 1.0.3 → 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 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
- ### Function Defaults
199
+ #### Associations
161
200
 
162
- In addition to static values, you can pass a function as a value:
201
+ If you want to override one or more fields based on a single value, use associations:
163
202
 
164
203
  ```ts
165
- const userFactory = createFactory({
166
- email: () => `example.${Math.floor(Math.random() * 1000)}@gmail.com`,
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
- Every time the factory is called, this will call the function and, in this case, generate a different `email` each time:
211
+ Then to generate a post associated with a user, use `with`:
172
212
 
173
213
  ```ts
174
- userFactory(); // { email: "example.424@gmail.com", ... }
175
- userFactory(); // { email: "example.133@gmail.com", ... }
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
- This is where [fake data generators](https://www.npmjs.com/search?q=fake%20data) and [sequences](#sequences) come in clutch:
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
- import { createFactory, createSequence } from "@aklinker1/zero-factory";
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
- .associate<User>("user", (user) => ({
262
- userId: user.id
263
- }))
295
+ }).associate("user", (user: User) => ({
296
+ userId: user.id,
297
+ }));
264
298
 
265
299
  const user = userFactory(); // { id: "user-0", ... }
266
- const postFactory.with({ user })(/* optional overrides */) // { id: "post-0", userId: "user-0", ... }
300
+ postFactory.with({ user })(/* optional overrides */); // { id: "post-0", userId: "user-0", ... }
267
301
  ```
@@ -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
@@ -43,20 +43,49 @@ function resolveDefaults(val) {
43
43
 
44
44
  // src/factories.ts
45
45
  function createFactory(defaults) {
46
- return createFactoryInternal(defaults, {});
46
+ return createFactoryInternal(defaults, {
47
+ traits: {},
48
+ associations: {}
49
+ });
47
50
  }
48
- function createFactoryInternal(defaults, traits) {
49
- return Object.assign((overrides) => generateObject(defaults, overrides), {
50
- many: (count, overrides) => generateManyObjects(count, defaults, overrides),
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), {
51
69
  trait: (name, traitDefaults) => createFactoryInternal(defaults, {
52
- ...traits,
53
- [name]: deepMerge(defaults, traitDefaults)
70
+ ...state,
71
+ traits: {
72
+ ...state.traits,
73
+ [name]: deepMerge(defaults, traitDefaults)
74
+ }
54
75
  }),
55
- ...Object.fromEntries(Object.entries(traits).map(([name, traitDefaults]) => {
56
- const traitFactory = (overrides) => generateObject(traitDefaults, overrides);
57
- traitFactory.many = (count, overrides) => generateManyObjects(count, traitDefaults, overrides);
58
- return [name, traitFactory];
59
- }))
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
+ ]))
60
89
  });
61
90
  }
62
91
  function generateObject(defaults, overrides) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aklinker1/zero-factory",
3
3
  "description": "Zero dependency object factory generator for testing",
4
- "version": "1.0.3",
4
+ "version": "1.1.0",
5
5
  "packageManager": "bun@1.2.20",
6
6
  "type": "module",
7
7
  "license": "MIT",