GithubHelp home page GithubHelp logo

fishery's Introduction

Fishery

CircleCI

Fishery is a library for setting up JavaScript objects for use in tests and anywhere else you need to set up data. It is loosely modeled after the Ruby gem, factory_bot.

Fishery is built with TypeScript in mind. Factories accept typed parameters and return typed objects, so you can be confident that the data used in your tests is valid. If you aren't using TypeScript, that's fine too โ€“ Fishery still works, just without the extra typechecking that comes with TypeScript.

Installation

Install fishery with:

npm install --save-dev fishery

or

yarn add --dev fishery

Usage

A factory is just a function that returns your object. Fishery provides several arguments to your factory function to help with common situations. After defining your factory, you can then call build() on it to build your objects. Here's how it's done:

Define and use factories

// factories/user.ts
import { Factory } from 'fishery';
import { User } from '../my-types';
import postFactory from './post';

const userFactory = Factory.define<User>(({ sequence }) => ({
  id: sequence,
  name: 'Rosa',
  address: { city: 'Austin', state: 'TX', country: 'USA' },
  posts: postFactory.buildList(2),
}));

const user = userFactory.build({
  name: 'Susan',
  address: { city: 'El Paso' },
});

user.name; // Susan
user.address.city; // El Paso
user.address.state; // TX (from factory)

Asynchronously create objects with your factories

In some cases, you might want to perform an asynchronous operation when building objects, such as saving an object to the database. This can be done by calling create instead of build. First, define an onCreate for your factory that specifies the behavior of create, then create objects with create in the same way you do with build:

const userFactory = Factory.define<User>(({ onCreate }) => {
  onCreate(user => User.create(user));

  return {
    ...
  };
});

const user = await userFactory.create({ name: 'Maria' });
user.name; // Maria

create returns a promise instead of the object itself but otherwise has the same API as build. The action that occurs when calling create is specified by defining an onCreate method on your factory as described below.

create can also return a different type from build. This type can be specified when defining your factory:

Factory.define<ReturnTypeOfBuild, TransientParamsType, ReturnTypeOfCreate>

Documentation

Typechecking

Factories are fully typed, both when defining your factories and when using them to build objects, so you can be confident the data you are working with is correct.

const user = userFactory.build();
user.foo; // type error! Property 'foo' does not exist on type 'User'
const user = userFactory.build({ foo: 'bar' }); // type error! Argument of type '{ foo: string; }' is not assignable to parameter of type 'Partial<User>'.
const userFactory = Factory.define<User, UserTransientParams>(
  ({
    sequence,
    params,
    transientParams,
    associations,
    afterBuild,
    onCreate,
  }) => {
    params.firstName; // Property 'firstName' does not exist on type 'DeepPartial<User>
    transientParams.foo; // Property 'foo' does not exist on type 'Partial<UserTransientParams>'
    associations.bar; // Property 'bar' does not exist on type 'Partial<User>'

    afterBuild(user => {
      user.foo; // Property 'foo' does not exist on type 'User'
    });

    return {
      id: `user-${sequence}`,
      name: 'Bob',
      post: null,
    };
  },
);

build API

build supports a second argument with the following keys:

  • transient: data for use in your factory that doesn't get overlaid onto your result object. More on this in the Transient Params section
  • associations: often not required but can be useful in order to short-circuit creating associations. More on this in the Associations section

Use params to access passed in properties

The parameters passed in to build are automatically overlaid on top of the default properties defined by your factory, so it is often not necessary to explicitly access the params in your factory. This can, however, be useful, for example, if your factory uses the params to compute other properties:

const userFactory = Factory.define<User>(({ params }) => {
  const { name = 'Bob Smith' } = params;
  const email = params.email || `${kebabCase(name)}@example.com`;

  return {
    name,
    email,
    posts: [],
  };
});

Params that don't map to the result object (transient params)

Factories can accept parameters that are not part of the resulting object. We call these transient params. When building an object, pass any transient params in the second argument:

const user = factories.user.build({}, { transient: { registered: true } });

Transient params are passed in to your factory and can then be used however you like:

type User = {
  name: string;
  posts: Post[];
  memberId: string | null;
  permissions: { canPost: boolean };
};

type UserTransientParams = {
  registered: boolean;
  numPosts: number;
};

const userFactory = Factory.define<User, UserTransientParams>(
  ({ transientParams, sequence }) => {
    const { registered, numPosts = 1 } = transientParams;

    const user = {
      name: 'Susan Velasquez',
      posts: postFactory.buildList(numPosts),
      memberId: registered ? `member-${sequence}` : null,
      permissions: {
        canPost: registered,
      },
    };

    return user;
  },
);

In the example above, we also created a type called UserTransientParams and passed it as the second generic type to define. This gives you type checking of transient params, both in the factory and when calling build.

When constructing objects, any regular params you pass to build take precedence over the transient params:

const user = userFactory.build(
  { memberId: '1' },
  { transient: { registered: true } },
);

user.memberId; // '1'
user.permissions.canPost; // true

Passing transient params to build can be a bit verbose. It is often a good idea to consider creating a reusable builder method instead of or in addition to your transient params to make building objects simpler.

After-build hook

You can instruct factories to execute some code after an object is built. This can be useful if a reference to the object is needed, like when setting up relationships:

const userFactory = Factory.define<User>(({ sequence, afterBuild }) => {
  afterBuild(user => {
    const post = factories.post.build({}, { associations: { author: user } });
    user.posts.push(post);
  });

  return {
    id: sequence,
    name: 'Bob',
    posts: [],
  };
});

After-create hook

Similar to onCreate, afterCreates can also be defined. These are executed after the onCreate, and multiple can be defined for a given factory.

const userFactory = Factory.define<User, {}, SavedUser>(
  ({ sequence, onCreate, afterCreate }) => {
    onCreate(user => apiService.create(user));
    afterCreate(savedUser => doMoreStuff(savedUser));

    return {
      id: sequence,
      name: 'Bob',
      posts: [],
    };
  },
);

// can define additional afterCreates
const savedUser = userFactory
  .afterCreate(async savedUser => savedUser)
  .create();

Extending factories

Factories can be extended using the extension methods: params, transient, associations, afterBuild, afterCreate and onCreate. These set default attributes that get passed to the factory on build. They return a new factory and do not modify the factory they are called on :

const userFactory = Factory.define<User>(() => ({
  admin: false,
}));

const adminFactory = userFactory.params({ admin: true });
adminFactory.build().admin; // true
userFactory.build().admin; // false

params, associations, and transient behave in the same way as the arguments to build. The following are equivalent:

const user = userFactory
  .params({ admin: true })
  .associations({ post: postFactory.build() })
  .transient({ name: 'Jared' })
  .build();

const user2 = userFactory.build(
  { admin: true },
  {
    associations: { post: postFactory.build() },
    transient: { name: 'Jared' },
  },
);

Additionally, the following extension methods are available:

  • afterBuild - executed after an object is built. Multiple can be defined
  • onCreate - defines or replaces the behavior of create(). Must be defined prior to calling create(). Only one can be defined.
  • afterCreate - called after onCreate() before the object is returned from create(). Multiple can be defined

These extension methods can be called multiple times to continue extending factories:

const sallyFactory = userFactory
  .params({ admin: true })
  .params({ name: 'Sally' })
  .afterBuild(user => console.log('hello'))
  .afterBuild(user => console.log('there'));

const user = sallyFactory.build();
// log: hello
// log: there
user.name; // Sally
user.admin; // true

const user2 = sallyFactory.build({ admin: false });
user.name; // Sally
user2.admin; // false

Adding reusable builders (traits) to factories

If you find yourself frequently building objects with a certain set of properties, it might be time to either extend the factory or create a reusable builder method.

Factories are just classes, so adding reusable builder methods can be achieved by subclassing Factory and defining any desired methods:

class UserFactory extends Factory<User, UserTransientParams> {
  admin(adminId?: string) {
    return this.params({
      admin: true,
      adminId: adminId || `admin-${this.sequence()}`,
    });
  }

  registered() {
    return this
      .params({ memberId: this.sequence() })
      .transient({ registered: true })
      .associations({ profile: profileFactory.build() })
      .afterBuild(user => console.log(user))
  }
}

// instead of Factory.define<User>
const userFactory = UserFactory.define(() => ({ ... }))

const user = userFactory.admin().registered().build()

To learn more about the factory builder methods params, transient, associations, afterBuild, onCreate, and afterCreate, see Extending factories, above.

Advanced

Associations

Factories can import and reference other factories for associations:

import userFactory from './user';

const postFactory = Factory.define<Post>(() => ({
  title: 'My Blog Post',
  author: userFactory.build(),
}));

If you'd like to be able to pass in an association when building your object and short-circuit the call to yourFactory.build(), use the associations variable provided to your factory:

const postFactory = Factory.define<Post>(({ associations }) => ({
  title: 'My Blog Post',
  author: associations.author || userFactory.build(),
}));

Then build your object like this:

const jordan = userFactory.build({ name: 'Jordan' });
factories.post.build({}, { associations: { author: jordan } });

If two factories reference each other, they can usually import each other without issues, but TypeScript might require you to explicitly type your factory before exporting so it can determine the type before the circular references resolve:

// the extra Factory<Post> typing can be necessary with circular imports
const postFactory: Factory<Post> = Factory.define<Post>(() => ({ ...}));
export default postFactory;

Rewind Sequence

A factory's sequence can be rewound with rewindSequence(). This sets the sequence back to its original starting value.

Contributing

See the CONTRIBUTING document. Thank you, contributors!

Credits

This project name was inspired by Patrick Rothfuss' Kingkiller Chronicles books. In the books, the artificery, or workshop, is called the Fishery for short. The Fishery is where things are built.

License

Fishery is Copyright ยฉ 2021 Stephen Hanson and thoughtbot. It is free software, and may be redistributed under the terms specified in the LICENSE file.

About thoughtbot

Fishery is maintained and funded by thoughtbot, inc. The names and logos for thoughtbot are trademarks of thoughtbot, inc.

We love open source software! See our other projects or hire us to design, develop, and grow your product.

fishery's People

Contributors

arianbessonart avatar b-houghton avatar bvarberg avatar dependabot[bot] avatar obliviousharmony avatar r3nya avatar rakeshpetit avatar sarahraqueld avatar siquel avatar stevehanson avatar tolgap avatar yuichkun avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fishery's Issues

Traits are ignored when providing build params

Taking FactoryBot as a comparison, I am trying to do something like: create(:user, :admin, confirmed: false), meaning I want an admin trait, but at the same time, I would to overwrite one of the default values for the returning object.

Now, when trying to do the same in Fishery:

import { Factory } from 'fishery';

class UserFactory extends Factory {
  admin() {
    return this.params({
      profile: {
        admin: true
      },
    });
  }
}

export default UserFactory.define(({ sequence }) => ({
  id: sequence,
  email: '[email protected]',
  registered: true,
}));

If I run this with default build: factory.admin().build(), I get what I expect:

{
  id: 1,
  email: '[email protected]',
  registered: true,
  profile: {
    admin: true
  }
}

But the moment I try to provide anything to the build: factory.admin().build({ registered: false }), I don't get admin attributes anymore:

{
  id: 1,
  email: '[email protected]',
  registered: false,
}

What I do want instead is:

{
  id: 1,
  email: '[email protected]',
  registered: false,
  profile: {
    admin: true
  }
}

I am not sure if it's a bug or an intended behaviour. If it's the latter, I'd like to understand the reasoning behind it. If I can't have slight variations on the traits or using traits with slightly different defaults, it makes using traits not that much useful, as the moment I need to have both, I just have to provide single build and write up everything I need from stratch

Error using traits

Trying to use the new traits feature following the docs and I'm getting an error that Property 'params' does not exist on type 'UserFactory' and Property 'sequence' does not exist on type 'UserFactory'. I'm fairly new to js/ts so it could be something stupid i'm doing.

import { Factory } from 'fishery';
import { Factories, User, UserTransientParams } from './types';
import * as faker from 'faker';

class UserFactory extends Factory<User, Factories, UserTransientParams> {
    admin(roleId: string) {
        return this.params({
            role: 'admin',
            roleId: roleId || `admin-${this.sequence()}`,
        });
    }
}


const userFactory = UserFactory.define(() => ({
    firstName: faker.name.firstName(),
    lastName: faker.name.lastName(),
    fullName: faker.name.firstName(),
    role: faker.lorem.word(),
    roleId: faker.lorem.word()
}));

`Date` params are hard to use with `DeepPartial`

Description

If you have a factory for a type with a Date attribute, the param is typed as DeepPartial<Date> which you can't use to override default values.

It seems like Date should possibly be one of the exceptions defined in deepPartial.ts because it doesn't make sense to have it as a partial (similar to Set, Map, etc)

To Reproduce

sandbox: https://codesandbox.io/s/fishery-test-forked-g8rbsi

type User = {
  name: string;
  createdAt: Date;
};

const userFactory = Factory.define<User>(({ params }) => ({
  name: 'Susan',
  createdAt: params.createdAt || new Date(),
/*
Type 'Date | DeepPartial<Date>' is not assignable to type 'Date'.
  Type 'DeepPartial<Date>' is not assignable to type 'Date'.
    Types of property 'toString' are incompatible.
      Type 'DeepPartial<() => string> | undefined' is not assignable to type '() => string'.
        Type 'undefined' is not assignable to type '() => string'.ts(2322)
*/
}));

Additional context
You can work around this by accessing createdAt or other Date attributes through associations, but this feels hacky

How to use rewind sequence

Hello, could you please update the readme with an example how to use the rewindSequence()? In my usecase I would need the sequence rewinded after the factory is built, so the next call starts it from beginning.

Extending factories receiving params and associations

Hi, i did not found a example to do something like this on the doc. I want to create some kind of factory with trait but without writing class.

Is this aproach ok?

const userFactory = Factory.define(({ sequence }) => ({
  id: sequence
}));

const baseFactory = notificationFactory.build()

const adminFactory = Factory.define(({associations})=>({
  ...baseFactory,
  admin: true,
  associatedTo: userFactory.build() || associations.associatedTo
}))

Expose the number in the list of the current instance in a `createList` or `buildList` scenario

Is your feature request related to a problem? Please describe.
I'd like to be able to know which item in the list is being built if a particular factory is called with createList or buildList.

Describe the solution you'd like
I'd like the value i in this code:

buildList(
    number: number,
    params?: DeepPartial<T>,
    options: BuildOptions<T, I> = {},
  ): T[] {
    let list: T[] = [];
    for (let i = 0; i < number; i++) {
      list.push(this.build(params, options));
    }

    return list;
  }

Exposed to the factory so I could do something where I build a set of objects with monotonically decreasing dates, for example. My use case is I wanted a set of objects to be built with a date field that was today's date minus i day(s).

Describe alternatives you've considered
I did a workaround where I reset the factory's sequence number in between test runs which works, but it feels like having the option to know the relative sequence number AND the i value from above would be awesome and relatively simple.

Additional context
Let me know if anything is unclear about my request.

Cannot find module './factories' or its corresponding type declarations.

Hello,

I'm trying to follow your documentation but struggling. Am new to JS/TS and of course to Fishery as well.

Am getting the above error on the import line:
import {factories} from './factories';

Is there some other file I need to drop in the factories folder? Please.

Thank you for your time.

Difficulty using objects with literal values in params

Hi - having some typescript problems where I'm trying to pass in an object parameter into the factory, where the type has a required field with a limited set of allowable values, but (I think) DeepPartialObject is making things tricky.

Example code:

interface UsageStatus {
  status: 'ok' | 'failed' | 'no_data'
  fromDate: string
  toDate: string
  lastAttempted?: string
}

interface BillingSite {
	siteStatus: string
    usageStatusData: UsageStatus
    // ...
}

export const billingSiteFactory = Factory.define<BillingSite>({params}) => {
  const { siteStatus, usageStatusData } = params 
  // ...
}

I get the following compile error:

TS2345: Argument of type '({ params: { siteStatus, usageStatusData, }, }: GeneratorFnOptions<BillingSite, any, BillingSite>) => { entity: "billingSite"; siteStatus: SiteStatus; siteType: SiteType.Residential; ... 9 more ...; usageStatusData: DeepPartialObject<...>; }' is not assignable to parameter of type 'GeneratorFn<BillingSite, any, BillingSite>'. ย ย 
  Type '{ entity: "billingSite"; siteStatus: SiteStatus; siteType: SiteType.Residential; siteId: string; licenceTransferPermission: { type: "none"; }; identity: {}; priceableSite: SitePricingDetails; ... 5 more ...; usageStatusData: DeepPartialObject<...>; }' is not assignable to type 'BillingSite'. ย ย ย ย 
    Type '{ entity: "billingSite"; siteStatus: SiteStatus; siteType: SiteType.Residential; siteId: string; licenceTransferPermission: { type: "none"; }; identity: {}; priceableSite: SitePricingDetails; ... 5 more ...; usageStatusData: DeepPartialObject<...>; }' is not assignable to type 'ResidentialBillingSite'.
 ย ย ย ย ย ย The types of 'usageStatusData.status' are incompatible between these types. ย ย ย ย ย ย ย ย Type '"ok" | "failed" | "no_data" | undefined' is not assignable to type '"ok" | "failed" | "no_data"'.
 ย ย ย ย ย ย ย ย ย ย Type 'undefined' is not assignable to type '"ok" | "failed" | "no_data"'.

From what it looks like, the deep partial is trying to make usageStatus.status accept an undefined, which it doesn't like.

Do you have any ideas or advice here? Thanks!

PS. We're on Fishery 2.2.2

How to define traits when not using typescript?

I am not using typescript, so the documentation is a bit hard for me to read. How would I define a trait for a simple factory that I export here:

import { Factory } from 'fishery';

export default Factory.define(({ sequence }) => ({
  id: sequence,
  manga_series_id: 1,
  name: 'MangaDex',
  seriesURL: 'example.com',
  lastVolumeAvailable: 1,
  lastChapterAvailable: 2,
}));

Use 'in transientParams' for defaults instead of destructuring

Is your feature request related to a problem? Please describe.

I was passing undefined to a transientParams, but it was getting set to the default value instead of literal undefined.

Describe the solution you'd like

Update these docs:

const { registered, numPosts = 1 } = transientParams;

To suggest:

const { registered } = transientParams;
const numPosts = 'numPosts' in transientParams ? transientParams.numPosts : 1;

Describe alternatives you've considered
N/A

Additional context
Happy to PR if welcome.

Symbol properties inside objects passed to the build function as associations are not getting merged into result

Description

If you call a factory's build function passing an object that contains symbols as properties, the symbols props won't be merged into the build result as seen here:

type User = {
  name: string;
};

type Post = {
  name: string;
  user: User;
};

const userFactory = Factory.define<User>(() => ({
  name: "Susan"
}));

const postFactory = Factory.define<Post>(() => ({
    name: "Post 1",
    user: userFactory.build()
  }));

const sym = Symbol("a");

const userWithSymbol = { ...userFactory.build(), [sym]: "a" };

const postFactoryResult = postFactory.build(
    {},
    { associations: { user: userWithSymbol } }
);

console.log(postFactoryResult.user[sym]) // it should return "a" but it's returning undefined instead because user doesn't have the symbol prop.

To Reproduce

I made this sandbox that showcases this behavior: https://codesandbox.io/s/fishery-test-forked-30eett?file=/src/index.test.ts

Additional context
After peaking around for a bit, I found out that this happens because the mergeCustomizer used in this library doesn't handle symbols, so it relies on lodash.merge behaviour which doesn't merge symbols by default as seen in this issue. For now, I found this workaround that does merge symbols into the resulting object:

const postFactory = Factory.define<Post>(({associations}) => ({
    name: "Post 1",
    user: associations.user || userFactory.build()
  }));

My guess is that it works because user gets the same object we passed that already has the symbols present on it, so when fishery starts merging our factory's return value with associations and params it doesn't remove the symbols that are already present on user.

Factory use will overwrite the created object of the previous use

When running tests in Jest with Typescript

Actual

Using the following factory:

import Big from 'big.js';
import { Factory } from 'fishery';
import { Swap } from '../lib/BalancerOpportunityDecorator/types';
import Token from './Token';

const srcToken = Token.build();
const destToken = Token.build();

export default Factory.define<Swap>(() => ({
  srcAmount:  Big(1),
  destAmount: Big(1),
  pool:       {
    id:      '',
    address: '',
    swapFee: Big('0.0015'),
  },
  srcToken,
  destToken,
  rate: Big(1),
}));

Like so:

  const swaps = [
    Factories.Swap.build({
      pool: {
        swapFee: Big('0'),
      },
      srcToken: {
        balance:  Big('25'),
        decimals: 18,
        weight:   Big('0.5'),
      },
      destToken: {
        balance:  Big('75'),
        decimals: 18,
        weight:   Big('0.5'),
      },
    }), Factories.Swap.build({
      pool: {
        swapFee: Big('0'),
      },
      srcToken: {
        balance:  Big('70'),
        decimals: 18,
        weight:   Big('0.5'),
      },
      destToken: {
        balance:  Big('30'),
        decimals: 18,
        weight:   Big('0.5'),
      },
    }),
  ];

Somehow makes swaps[0] be an equal object to swaps[1], where swap[1] passed in properties are the ones used.

ie. swaps[0].srcToken.balance === swaps[1].srcToken.balance === Big('70') as well as the other properties.

Expected

swap[0] !== swap[1]

swaps[1].srcToken.balance === Big('25')

Help?

buildList and createList typing doesn't outrule empty array from results

Is your feature request related to a problem? Please describe.
In our codebase we use fishery a lot and love it. But we also love strict types and are now having an issue with fishery and a utility type we use, NonEmptyArray. A very short way to reproduce this is:

// Utility type
type NonEmptyArray<T extends any> = [T, ...T[]]

// Domain object types
type Bar = {
  baz: number
}

type Foo = {
  bars: NonEmptyArray<Bar>
}

// Factories
const barFactory = Factory.define<Bar>(() => ({
  baz: 1
}))

const fooFactory = Factory.define<Foo>(() => ({
  bars: barFactory.buildList(3) // TS2322: Type 'Bar[]' is not assignable to type 'NonEmptyArray<Bar>'. ย ย Source provides no match for required element at position 0 in target.
}))

Describe the solution you'd like
The definitions of buildList and createList could have function overloads to state that

  • if the number of items is 0, the return type is never[] (never[] can always be assigned to T[], link to playground)
  • if the number is not 0, the output type is [T, ...T[]]

Describe alternatives you've considered
I know this is not the biggest issue as it can be bypassed by just replacing fooFactory.buildList(5) with [fooFactory.build(), fooFactory.build(), fooFactory.build(), fooFactory.build(), fooFactory.build()]. But doing this really sucks, and I don't see any downsides with the overloads so I'll open a PR about it

Guidance on Production Use?

Thanks for contributing Fishery to the world!
How production-ready is this library? I don't get a lot of confidence from the description in package.json ("A library for setting up JavaScript factories to help build objects as test data, with full TypeScript support") and the docs that it is for much more than testing.
I'm using typeorm in a project, and right now the factory functions which create entity instances in front end code are kind of ad hoc, and this would seem to fill that role nicely.
I also use some custom TS code to type Entities based on which associated Entities were loaded with them, so it sounds like maybe I should wait to see what this more dynamic rewrite will bring.

Forcing an empty array

Hey, thanks for building the fishery, it's awesome!

I have a question about forcing the empty array of elements:

import { Factory } from "fishery";

type Elements = {
  elements: string[];
};

const elements = Factory.define<Elements>(() => ({
  elements: ["STRING"],
}));

console.log(elements.build({ elements: [] }));
// { elements: ["STRING"] }

The result of the execution will be non-empty array while I expect the empty array for property elements. Is the array does not set to an empty array by design? If it's not expected, mind pointing me where this should be fixed, so I'll be able to open the PR.

Again, thanks for the awesome library, it's absolute pleasure to use it โค๏ธ

Factory produces data incompatible to its type constraint

Description

When using the factory overrides to set a property explicitly to undefined this is always allowed even when the type constraint doesnโ€™t accept undefined as a valid value.

To Reproduce

import { Factory } from 'fishery';

interface Foo {
    bar: string;
}

const fooFactory = Factory.define<Foo>(() => {
    return { bar: 'any-value' };
});

const result = fooFactory.build({ bar: undefined }); // result has the type Foo but its value is { bar: undefined } which is not a valid Foo

Additional context

It seems like this bug has been introduced with #44

Confusion about updating associations dynamically

I'm trying to figure out how to dynamically build associations, and I'm a bit confused about the API and what the correct way to handle this is.

My primary use case is we have a complex data model where we need to call multiple traits to set up a variety of associations. These traits can't use associations() though, because we need to generate the association in the context of a specific record (i.e. we need to be able to set foreign keys).

This is an example that I have been working with:

 withQaReports() {
    return this.onCreate(workflow => ({
      ...workflow,
      qualityAssuranceReports: qualityAssuranceReportFactory.buildList(
        2,
        {},
        {
          associations: {
            assignment: assignmentFactory.build({ workflowId: workflow.id }),
          },
        },
      ),
    }));
  }

I'm running into a few issues that either may be my misunderstanding and/or possible bugs:

  1. The different lifecycle hooks are confusing. From the documentation, onCreate and afterCreate seem to be specifically for async side-effects. But out of the 3 lifecycle hooks, only onCreate seems to update the factory result from the hook's return value. Is this the intended result / use case? It's just awkward to have to await a factory when I'm not using any async behavior.

  2. Chaining traits doesn't seem to work correctly. If I have a factory with multiple traits that update associations via onCreate, only the last called trait will stick. This seems like it is either a bug or I'm not using the hook as intended. I missed in the documentation that onCreate can only be defined once. Given this limitation, this seems like a poor option for my use case.

Is there a way to use composable traits that update the shape of the object?

Thanks!

Finalize API

I'm trying to settle on an API for defining factories. The two choices I'm thinking about right now are:

Function-based

In this approach, a function is passed to Factory.define which takes various parameters that can be used for hooks and such. The define function returns an instance of our Factory class on which we can call build and buildList, etc.

I like how succinct this approach is, especially with simpler factories. Defining the factory does feel a little clunky though as we add more options, and I don't really like defining the afterCreate before the return value:

const userFactory = Factory.define<User>(({ sequence, afterCreate }) => {
  // TypeScript knows the type of `user` here ๐ŸŽ‰
  afterCreate(user => {
    user.posts = buildSomePosts();
  })

  return {
    id: `user-${sequence}`,
    name: 'Susan',
  };
});

const user = userFactory.build({ id: '5' })

Simpler function-based with object argument

This is like the above approach, but the define method takes an object instead of a function. This makes the API seem less clunky, and individual properties can still utilize functions in order to be dynamically computed. This is similar to aexmachina/factory-girl.

Update: can't think of a way for this to work with TypeScript.

const userFactory = Factory.define<User>({
  id: sequence(n => `user-${n}`),
  name: 'Bob',
  date: () => new Date(),
});

Class-based, using inheritance

An alternative approach is to define factories by extending a base Factory class. It feels very similar to how React works with extending Component<Props...>. Another benefit is that it nicely separates the various hooks into their own methods.

A negative is that the argument in afterCreate (and other hooks that we add) is not automatically typed by TypeScript, since TypeScript doesn't know if we are intending to overload from Factory or define a new method (just like how React prevProps is not auto-typed in componentDidUpdate).

class UserFactory extends Factory<User> {
  buildObject() {
    return {
      id: `user-${this.sequence()}`,
      name: 'Bob',
    };
  }

  afterCreate(user: User) {
    user.posts = buildSomePosts();
  }
}

const userFactory = new UserFactory();
const user = userFactory.build({ id: '5' })

Conclusion

I'm not sure yet which way I'm leaning. I might try to think some more on what sort of features might be added in the future in order to determine which approach will be the most accommodating.

factory.create() expected usage with an ORM or 3rd party API

I'm integrating fishery with Sequelize (https://sequelize.org/), and am not sure if I'm taking the correct approach... Is this the expected usage?

export type Organization = {
  id: number
  name: string
}

class OrganizationFactory extends Factory<Organization> {}
export const organizationFactory = OrganizationFactory.define(({ sequence, params, onCreate }) => {
  onCreate((org) => {
    return models.Organization.build({ ...org }).save()
  })

  return {
    id: sequence,
    name: params.name || `A New Org ${sequence}`,
  }
})
const { id } = await organizationFactory.create()
const organization = await models.Organization.findByPk(id)

Is there a way to return the created database object back from someFactory.create() calls? So this reads like...

const organization = await organizationFactory.create()
// organization is a Sequelize object.

A different approach was taken here - https://paramander.com/en/blog/extending-fishery-to-build-test-data-for-sequelize-models but seems to miss onCreate hooks since super.create is not called in the mixin?

Thanks!

Support for required values typing on build/create, not on define

We have a bunch of factories mapped to various DB structures. As is the case for most projects, the DB has some required fields and some optional ones. So we created a Typescript type indicating which were required/optional, and passed them into Fishery.

However, the factory definition can't specify some of these attributes up front (e.g. a post factory might have an author_id which needs to be passed into it). However, the author_id is still required when we create the record. But fishery doesn't seem to support this case.

e.g.

type User = {
  title: string
  author_id: string
}

const userFactory = Factory.define<User>(({ onCreate }) => {
  onCreate(attrs => User.create(attrs));

  return {
    title: 'Testing'
  }
})

const author = { id: 123 } // normally created via another factory

const user = await userFactory.create({ author_id: author.id })

We are providing the title for the post, but the author_id needs to be passed in on create.

However, the Factory typing barfs and as a result, the create call also complains.

It would be great if fishery types could accept any attributes when defining the factory, and only check typing on build/create

In the meantime, to get everything working, I have to make all values optional:

type User = {
  title?: string
  author_id?: string
}

but this obviously leads to issues where required attributes are missed

DeepPartial type cannot handle 'unknown' type

Given the following example:

import { Factory } from 'fishery';

interface Entity1 {
  entity2: Entity2;
}

interface Entity2 {
  id: string;
  entity3: Array<Entity3>;
}

interface Entity3 {
  _permissions: unknown;
}

const entity2Factory = Factory.define<Entity2>(() => ({
  id: 'abc',
  entity3: [],
}));

const entity2 = entity2Factory.build();

const entity1Factory = Factory.define<Entity1>(() => ({
  entity2: entity2Factory.build(),
}));

const entity1 = entity1Factory.build({
  entity2: entity2,
});

I receive the following TypeScript error:

TS2345: Argument of type '{ entity2: Entity2; }' is not assignable to parameter of type 'DeepPartial<Entity1>'.
   The types of 'entity2.entity3' are incompatible between these types.
      Type 'Entity3[]' is not assignable to type '(DeepPartial<Entity3> | undefined)[]'.
         Type 'Entity3' is not assignable to type 'DeepPartial<Entity3>'.
            Types of property '_permissions' are incompatible.
               Type 'unknown' is not assignable to type 'DeepPartial<unknown> | undefined'.
                  Type 'unknown' is not assignable to type 'DeepPartial<unknown>'.

DeepPartial does not support nullable Arrays

Hello ! We ran recently in this issue regarding the following snippet

import { Factory } from 'fishery'

type Request = {
  queryParams: {
    [name: string]: string[] | undefined | null
  }
}

export const requestFactory = Factory.define<Request>(({ params }) => {
  const { queryParams = {} } = params

  return {
    queryParams,
  }
})

TypeScript will output an error

Argument of type '({ params }: GeneratorFnOptions<Request, any>) => { queryParams: DeepPartial<{ [name: string]: string[] | null | undefined; }>; }' is not assignable to parameter of type 'GeneratorFn<Request, any>'.
  Call signature return types '{ queryParams: DeepPartial<{ [name: string]: string[] | null | undefined; }>; }' and 'Request' are incompatible.
    The types of 'queryParams' are incompatible between these types.
      Type 'DeepPartial<{ [name: string]: string[] | null | undefined; }>' is not assignable to type '{ [name: string]: string[] | null | undefined; }'

This is because the DeepPartial type does not fallback correctly on null and undefined values

export type DeepPartial<T> = {
  [P in keyof T]?: unknown extends T[P]
    ? T[P]
    : T[P] extends Array<any>
    ? T[P]
    : DeepPartial<T[P]>;
};

Should be

export type DeepPartial<T> = {
  [P in keyof T]?: unknown extends T[P]
    ? T[P]
    : T[P] extends Array<any> | undefined | null
    ? T[P]
    : DeepPartial<T[P]>;
};

Thank you for the great job anyway

Extending a factory with new fields

Is your feature request related to a problem? Please describe.
First of all, I love this project! You say it was built with Factoy Boy as an inspiration, but I work with Factory Boy on a regular basis in a Django project and Fishery is SO much easier to work with. Not to mention the full typescript support that just makes it a joy to use. I've only got one issue I can't figure out if it's possible or not with the current toolset.
In our production project we have various different types of nodes, which are represented as data objects. Each of these nodes have some common fields and a few specific fields. I'm trying to figure out if I can build a standard, common Node factory and then define separate factories for each node with the common core as a shared fieldset.
For example:

const coreFactory = Factory.define<Core>(() => {
  return {
    field1: 'this',
    field2: 'that',
})

const specificFactory = Factory.define<Specific>(() => {
  return {
     ...fieldsOfCoreFactory,
     field3: 'more',
     field4: 'and more',
  }
}

Describe the solution you'd like
I love the way params and transientParams work, but as far as I can tell they don't allow me to define new return fields. I also don't know if extending the factory class could do what I need it to do. It would be great if there was a way to extend the returned object from the Factory without resorting to tricks such as deep cloning shared objects into the return field. Perhaps an .extend() method to do just that, like how you can use .params() to create new outcomes based on existing fields?

Describe alternatives you've considered
I've thought about merging return objects by deep cloning, but it requires creating separate objects outside of factories just for this reason. It also seems to mess with the types if I do that, though that could just have been me.

Again, thank you for building this awesome package! I've read it is a side project for you, so it's understandable if my request isn't in the cards right now. If you know of another workaround in the meanwhile, I'm open to that too!

Support for flow types

We observed that the library is not able access flow types. Is that something known and you have plans to add it at some point later or would it only support TypeScript ? Would be happy to pair if it works and needs some configuration in the project.

Is there a way to specify the type when extending factories?

Given the following types:

interface BaseUser {
  id: number;
  isAdmin: boolean;
}

interface NormalUser extends BaseUser {
  isAdmin: false;
}

interface AdminUser extends BaseUser {
  isAdmin: true;
  adminPrivileges: string[];
}

type User = NormalUser | AdminUser;

Is there a way to create a factory for the type User and then a factory for the type AdminUser which re-uses the User factory but has the correct type information, so the AdminUser factory returns objects of the type AdminUser?

As far as I understand we can only extend the factories like so:

const userFactory = Factory.define<User>(() => {
  return {
    id: 42,
    isAdmin: false
  };
});

const adminFactory = userFactory.params({ isAdmin: true, adminPrivileges: [] });

In this case adminFactory returns the type User.

My suggestion would to make the following work:

const userFactory = Factory.define<User>(() => {
  return {
    id: 42,
    isAdmin: false
  };
});

const adminFactory = userFactory.params<AdminUser>({ isAdmin: true, adminPrivileges: [] });

So that adminFactory always returns objects of type AdminUser and only accepts params of DeepPartial<AdminUser>.

Overwriting params whose values are arrays

I've got an object whose interface looks like this:

interface IPallet {
  id: number
  pictures: IPicture[]
}

and I've created a factory like this:

import { Factory } from 'fishery'
import { IPallet, IPicture }  from '../types'

export default Factory.define<IPallet>(({ params, sequence }) => ({
  id: sequence,
  pictures: (params.pictures as IPicture[]) || [{ uri: 'abc123' }],
}))

the type coercion happening in pictures (params.pictures as IPicture[]) is something else I don't understand, but my main question is about overwriting params whose values are arrays; it seems I'm not able to overwrite the default pictures value as I expected I'd be able to, namely:

import palletFactory from '../factories/pallet'
...
const pallet = palletFactory.build({ pictures: [] })

using this, the pictures attribute of pallet would still be [{ uri: 'abc123' }].

Unless I'm doing something wrong, the only way I can successfully do this is by using associations.

Questions:

  1. am I doing something wrong? Happy to provide more detail if this isn't sufficient.
  2. is there some convention being applied to attributes whose values are arrays that forces them to be associations? Or is that a side effect of something else?

I think the readme could be updated to address this - I'd be happy to do that once I understand what's going on here!

Factory extensions should provide generatorOptions as a parameter

This would be extremely helpful for building a shared factory passed between an objection-using client and a non-objection using client. The database one would have certain associations or transient params relating to the relational properties defined by its objection model and used within create hooks that tell the database how to save the built object.

For example, say you have a rectangle factory and a canvasFactory that contains rectangles. This factory takes max and min values for its boundaries.

const rectangleFactory = Factory.define<Rectangle, RectangleTransientParams>(({transientParams, associations}) => {
	const min = transientParams.min || 0;
	const max = transientParams.max || 5;
	const x = faker.datatype.number({min, max});
	const y = faker.datatype.number({min, max});
	const x2 = faker.datatype.number({min, max});
	const y2 = faker.datatype.number({min, max});
	return {
		x: Math.min(x, x2),
		y: Math.min(y, y2),
		w: Math.abs(x2 - x),
		h: Math.abs(y2 - y),
		canvasId: associations.canvas ? associations.canvas.id : undefined,
	}
});

Now, lets say that a rectangle has a canvasId that references its parent canvas. It must have a parent canvas. However, some things that utilize this rectangle don't have database access to save these values for the test runner. For those, we extend the factory with a nice .withFakeDatabaseValues() hook that fills in those values, and we're good to go.

However, in database land, we have an issue. We want to extend the factory like this to allow us to create rectangles with their dependency trees in tact without having to create every member of the dependency tree explicitly beforehand. So, we want something like this:

rectangleFactory.onCreate(async rect => {
	if(!rect.canvasId){
		const newCanvas = await canvasFactory.create({transientParams: {max: transientParams.max, min: Math.max(rect.x+rect.w, rect.y+rect.h)}});
		rect.canvasId = newCanvas.id;
	}
	return await Rectangle.query().insert(rect).returning('*');
})

Our canvas has transient params for a maximum dimension size and for a minimum dimension size just like the rect does. The minimum size is determined by the rect, since it has to contain the rect, but we don't want it always wrapping exactly at the edge of the rect. So the maximum is based on the original maximum fed to the rect.

Currently, there's no way to get that transientParam outside of the original define() call for rectangle factory, making this database code have to be dependency injected into a factory generator function that ends up being really weird and ugly. I personally think that the generatorOptions that are given to define should just be passed to the extensions.

rectangleFactory.onCreate(async (rect, {transientParams}) => {
	if(!rect.canvasId){
		const newCanvas = await canvasFactory.create({transientParams: {max: transientParams.max, min: Math.max(rect.x+rect.w, rect.y+rect.h)}});
		rect.canvasId = newCanvas.id;
	}
	return await Rectangle.query().insert(rect).returning('*');
})

Build params mutate nested objects

Description

In my project I use a couple of factories for different types to define a coherent set of data. For each type, I create a list of instances which may be referenced by other types:

const userFactory = Factory.define<User>(() => ({
    id: datatype.uuid(),
    firstName: name.firstName(),
    lastName: name.lastName(),
}));

const users = userFactory.buildList(20); // ๐Ÿ‘ˆ generate 20 random users

const taskFactory = Factory.define<Task>(() => ({
    title: lorem.words(),
    completed: datatype.boolean(),
    user: random.arrayElement(users) // ๐Ÿ‘ˆ randomly select one of the 20 users
}));

Sometimes, I also need to create variants of the same task:

const task = taskFactory.build();
const completedTask = taskFactory.build({ ...task, completed: true });

(In this simplified use-case it would be of course easier to just skip the factory and just write const completedTask = { ...task, completed: true }, but let's assume, that in real-world projects there might be good reasons to stick with the factory.)

That code seems to be harmless, but it actually mutated other users within my list of users which took the better part of my day to realize :)

From what I understand, this is what happens:

  • the taskFactory creates a new task with a random user from my list of 20 users
  • the build method then deep-merges params (including the nested user of task) into this new task (_mergeParamsOntoObject in FactoryBuilder.build in Line 65)
  • the deep-merge mutates the nested user instance within the newly generated task and thereby also mutates the contents of the users array

It might be up for debate if this is really a bug. But that behavior leads to side-effects that might be unintended and are hard to debug.

To Reproduce

Code Sandbox: https://codesandbox.io/s/fishery-test-forked-0rjz3?file=/src/index.test.ts

Alternatively: https://github.com/luchsamapparat/fishery-mutation-reproduction

Additional context

Modifying _mergeParamsOntoObject to merge into an empty object should solve the issue:

  _mergeParamsOntoObject(object: T) {
    merge({}, object, this.params, this.associations, mergeCustomizer);
  }

Subset of items for buildList should be build for each item

Description

First of all, thank you guys for your hard work, this is really cool lib!
During the integration with the fishery, we found one behavior that seems wrong.
Imagine that we have Users with Posts and I what to generate a list of users with random posts, let's say 5 users with 5 posts.
To make it more useful I've done it like this:

type Post = {
  id: number;
}
type User = {
  id: number;
  posts: Post[];
};

const postFactory = Factory.define<Post>(({ sequence }) => ({ id: sequence }));

class UserFactory extends Factory<User> {
  withPosts(posts?: Post[]){
    return this.params({
      posts: posts || postFactory.buildList(5),
    })
  }
}


const userFactory = UserFactory.define(({ sequence }) => {
  return {
    id: sequence,
    posts: [],
  };
});

const users = userFactory.withPosts().buildList(5);

So after I created a list of users I expected to have 25 different posts, but I have 5 users with the same 5 posts.

To Reproduce

CodeSandbox with a failing test

DeepPartial doens't partial nested arrays

repl here

having an object with a collection inside it doesn't work

// each fish has 10 keys ( name, age, favorite food, whatever)
const collectionFactory  = Factory.define({
  fish: [{...}]
})

// breaks
cost foo = collectionFactory.build({ fish: [ {name: 'jeff'}])

even if you account for it, it still breaks:

interface Fish {
  name: string
  fins: number
  hasChildren: boolean
}

interface School {
  fishType: string
  members: Fish[]
}

const fishFactory = Factory.define<Fish>(() => ({
  name: 'jacob',
  fins: 2,
  hasChildren: false
}))

const schoolFactory = Factory.define<School>(({params}) => ({
  fishType: 'makrel',
  // note this
  members: params.members ? params.members.map(m => fishFactory.build(m)) : fishFactory.buildList(1)
}))

const school = schoolFactory.build({ members: [{name: 'meg'}) // TypeError: {name: string} is not assignable to Fish, missing fins, hasChildren

just write schoolFactory.build({member: fishFactory.buildList(1, {name: 'meg'})

sure, but that is missing the point. I might not want to expose a factory for fish because it doesn't make sense and requires familiarity with it to use.

maybe something like this would work?

const factory = Factory.define(({list}) => ({
   list: list({ name: 'jacob', fins:2, hasChildren: false})
}))

Async Support

I'm trying to integrate this library with TypeORM, and I'd like to make factories as follows:

export const userFactory = Factory.define<User, Factories>(({ afterCreate }) => {
  afterCreate(async (user) => {
    return await manager.save(user);
  });

  const user = getRepository(User).create({
    username: randomString(),
  });

  return user;
});

However, afterCreate does not support asynchronous callbacks, which means that client code needs to save after building. It would be great to be able to handle saving in the factory like in factory_bot!

Using one property's value in another

Given the factory:

export default Factory.define<User>(() => ({
  firstName: 'John',
  lastName: 'Smith',
  fullName: `${firstName} ${lastName}`
}));

How can I use firstName and lastName to form the value for fullName dynamically?

Workaround for Deadlock or sequence uniqueness problem

I was facing a Deadlock errors in tests, how to achieve it:

  • Jest runs multiple tests in parallel
  • different tests are operating with the same Factories, tests are creating, editing, deleting rows in same tables
  • sequence is used as id and always starts from 1, which means different tests are creating records with the same ids

I didn't encounter uniqueness problem because all tests are running in own transactions in my setup, but instead I had Deadlock errors because different transactions are operating with records with the same ids.

And here is simple workaround for Jest:

const jestWorkerId = parseInt(process.env.JEST_WORKER_ID as string);
if (!jestWorkerId) {
  throw new Error('Cannot get JEST_WORKER_ID env variable');
}

const idsDistanceBetweenWorkers = 1000;

Object.assign(Factory.prototype, {
  sequence(this: { id: { value: number } }) {
    // id starts from 1 in fishery
    if (this.id.value === 1) {
      this.id.value = jestWorkerId * idsDistanceBetweenWorkers;
    }

    return this.id.value++;
  },
});

Jest provides JEST_WORKER_ID starting from 0 which means index of current running in parallel test

I monkey-patched sequence method to start from 0 for first worker, from 1000 for second worker, from 2000 for third and so on.

Hope this can help someone

I propose to change SEQUENCE_START_VALUE constant to be a public field of Factory so it can be set without monkey-patching

Loose inferred return type of generator function

Description

The inferred type of the generator allows for extra properties(variant 1).
In the case of class extension it also requires narrowing the literal types(variant 3).
It seems to work well when the return type is set explicitly(variant 2, variant 4).

Looking at the source code(the generic types), I would expect the generator return type to be inferred properly ๐Ÿค”

I'm not sure if this is a bug or a feature ๐Ÿ˜„

image

To Reproduce

https://stackblitz.com/edit/fishery-test?file=Post.ts

Additional context
Fishery version: 2.2.2

Composable Factories?

Is it possible to do composition for Factories in here?

Since FactoryBot is a point of reference for this project, I'm looking for something like traits, where I can generate variations from a "Base Factory".

Uniqueness conflicts with auto-incrementing IDs in Postgres

I'm trying to use Fishery in a project that also uses Prisma, an ORM that follows the data mapper pattern. I've defined a user factory as follows:

import { Factory } from "fishery";
import { User } from "@prisma/client";
import prisma from "../client";

export default Factory.define<User>(({ sequence, onCreate }) => {
  onCreate(user => {
    return prisma.user.create({ data: user });
  });

  return {
    id: sequence,
    ...
  };
});

The id is a required property of a User, so it must be specified here. However, id is also an auto-incrementing column in Postgres โ€“ and when you manually specify an ID when inserting a row, Postgres does NOT automatically update the auto-incrementation value. As a result, the following can happen:

  • The user factory creates a User with ID 1
  • Additional actions cause a User to be created via more typical means, with no specified ID
  • Postgres attempts to assign this new User an auto-incrementing ID of 1
  • The DB operation fails due to a uniqueness violation, and the second User is not saved

Possible solutions

Ideally, we could omit id: sequence from the user factory. However, in this case the Prisma User type requires a valid value for id. (Prisma actually doesn't have the concept of an "unsaved User object", as far as I can tell โ€“ instead it uses a separate type of UserCreateInput when referring to a collection of attributes that could be inserted into the database and turned into a full-fledged User.)

One possible solution might be to update Fishery such that it allows build to return a UserCreateInput object, while allowing create to return a different type of User/Promise<User>. (Maybe this is possible already? But if so, I couldn't figure out how to do it from the documentation.) This would make Fishery compatible with ORMs that use different types for saved and unsaved objects.

Alternately, there might be a different solution that's much more appropriate! I'm definitely guessing a bit here โ€“ I'm primarily a Ruby developer, and not nearly as familiar with modern JS. If there are other suggested approaches for how to handle this issue, please let me know!

DeepPartial does not work with generic constraints

I am having an issue with DeepPartial and the use of generic constraints in Typescript v4.3.5

This is a simple scenario (while this error also occurs within the Factory implementation, I simplified the scenario for brevity)

interface Base {
  id: number;
}

interface Child extends Base {
  value: string;
}

class Test<T extends Base> {
  log() {
    // Fails with Type '{ id: 0; }' is not assignable to type 'DeepPartial<T>'.
    const x : DeepPartial<T> = {id: 0};

    // Success
    const y: DeepPartial<Base> = {id: 0};
  }  
}

const childTest = new Test<Child>();

createList contains duplicate sequence values

Hi ๐Ÿ‘‹ I'm using version 1.4.0 and I think I've found an issue with the createList method. Using it sometimes results in duplicate sequence values. This is my test code:

  describe('findActiveUsers', () => {
    describe('as admin', () => {
      beforeEach(async () => {
        currentUser = await userFactory.roleAdmin().create();
      });

      it('returns all active users', async () => {
        const activeUsers = await userFactory.createList(5);
        console.log(currentUser.id);
        console.log(activeUsers.map((u) => u.id));
     // ...

This results in the following log output:

1
[2, 3, 2, 5, 4]

As you can see there is a duplicate id. I'm using TypeORM and the user entity has an id attribute that is a @PrimaryGeneratedColumn(). Could that be an issue? I think it shouldn't be, because I'm assigning the factory sequence to the user's id attribute:

class UserFactory extends Factory<User> {/* some traits */}

export default UserFactory.define(({ sequence, onCreate }) => {
  onCreate(async (user) => {
    return getRepository(User).save(user);
  });

  return getRepository(User).create({
    id: sequence,
    // more attributes
  });
});

๐Ÿคทโ€โ™‚๏ธ

Question: how to share onCreate code for multiple factories?

hey,

after each factory.create i would like to send API request on the data.
since this is the same code sent to API, looking for a generic onCreate mechanism.

i achieved it by this code, but it doesn't look clean to me:


export default extendWithOnCreate(
  Factory.define<any>(({ sequence }) => ({
    id: sequence,
    name: 'Fleet1',
    description: 'Fleet`s Description',
  })),
  createFleetDocument
);



export function extendWithOnCreate(factory: any, mutationDocument: any) {
    factory._onCreate = (async (entity: any) => {
        const client = createApolloClient();

        await client.mutate({
        mutation: mutationDocument,
        variables: { ...entity }
        })
        .catch((e) => {
            console.log(JSON.stringify(e, null, 2));
            throw e;
        });

        return entity;
    });

    return factory;
}
  

Using any of the factory extension methods breaks sequence

Using any of the extension methods (params, associations, etc.) seems to causes the sequence to stop incrementing. I'm wondering if this is by design or not and if there is a recommend approach. I see you can explicitly call it by extending Factory but this creates a situation where I either have to know if given function increments the sequence or remember to include it in each function to be safe. I have a feeling that this has something to do with the clone implementation but figured I'd ask before I got too far. Thanks in advance!

Here's an example...

interface Model {
  id: number,
  prop1: number,
  prop2: string
}
class ModelBuilder extends Factory<Model> {
  withSameValue(prop: number) {
    return this.params({
      prop1: prop,
      prop2: prop.toString()
    });
  }
}

export const modelBuilder = ModelBuilder.define(({sequence}) => ({
  id: sequence,
  prop1: 10,
  prop2: "test"
}));

The following code...

const model1 = modelBuilder.withSameValue(5).build();
console.log(model1);
const model2 = modelBuilder.withSameValue(6).build();
console.log(model2);

...outputs...

LOG LOG: Object{id: 1, prop1: 5, prop2: '5'}
LOG LOG: Object{id: 1, prop1: 6, prop2: '6'}

The following code...

const model1 = modelBuilder.build();
console.log(model1);
const model2 = modelBuilder.build();
console.log(model2);

...outputs...

LOG LOG: Object{id: 1, prop1: 10, prop2: 'test'}
LOG LOG: Object{id: 2, prop1: 10, prop2: 'test'}

Type error when using the third parameter for `define<...>`

Hi! ๐Ÿ‘‹

I'm facing a type error when I define the third parameter to be used upon using .create(). My first type, Prisma.OrganizationCreateInput, is a Prisma generated input payload type. It has two required properties: slug, and name. The third parameter, Organization, is as well a Prisma generated type. This is the type used for successfully created (or fetched) entities. In addition to the mandatory fields in Prisma.OrganizationCreateInput, Organization also contains the property id as required. I can't quite understand why I'm getting this type error when it seems that my setup is pretty much exactly like in the examples.

Any tips would be appreciated!

I'm using [email protected].


Code:

import { Factory } from 'fishery';
import faker from 'faker';
import db from '../lib/db';
import { Prisma, Organization } from '@prisma/client';

export default Factory.define<
  Prisma.OrganizationCreateInput,
  any,
  Organization
>(({ onCreate }) => {
  onCreate((organization) => db.organization.create({ data: organization }));

  const organizationName = faker.company.companyName();

  return {
    name: organizationName,
    slug: faker.helpers.slugify(organizationName),
  };
});

Error:

The 'this' context of type 'typeof Factory' is not assignable to method's 'this' of type 'new (generator: GeneratorFn<OrganizationCreateInput, any>) => Organization'.
  Types of construct signatures are incompatible.
    Type 'new <T, I = any>(generator: (opts: GeneratorFnOptions<T, I>) => T) => Factory<T, I>' is not assignable to type 'new (generator: GeneratorFn<OrganizationCreateInput, any>) => Organization'.
      Type 'Factory<OrganizationCreateInput, any>' is missing the following properties from type 'Organization': name, slug

Remove existing value with undefined

Sometimes you want to remove existing data, but it seems like null and undefined are handled differently. The code below returns { firstname: 'First', lastname: null }, but i would expect { firstname: undefined, lastname: null }.

import { Factory } from "fishery";

type MyType = {
  firstname?: string;
  lastname: string | null;
};

export const myFactory = Factory.define<MyType>(() => ({
  firstname: "First",
  lastname: "Last",
}));

console.log(
  myFactory.build({
    firstname: undefined,
    lastname: null,
  })
);

How to use onCreate

Hello again,

Can you explain a bit more about how to use onCreate hook. It seems the docs assume familiarity or previous hands on experience with Factory Bot? Which I don't have.

Fishery doesn't, out-of-the-box, actually create a record in the database right? That would be something extra that would have to be hand coded? The documentation I've read on Ruby's Factory Bot just says that the model/record gets created in the database if one uses create vs. build.

So, for example, for Fishery I would have to somehow stitch up usage of axios, for example, to make an API call to create some record and then wire up that API call to Fishery's Create/onCreate methods?

Or am I way off here? Again, thank you very much for any assistance you can grant.

Sequence start at 0...

Starting to use Fishery recently and noticed an unexpected behaviour. The sequence that factories start with is 0, however, FactoryBot's sequences start with 1. I believe 1 is a better choice, as all of our Fishery's factories have to do:

export default Factory.define(({ sequence }) => ({
  id: sequence + 1,
  // other attributes set here...
}))

Let me know if I'm missing something.

Dynamic Object Types

I've looked around and don't think this is possible to do given how fishery is set up, but figured I would mention it.

I'm wondering if it makes sense for fishery to allow dynamic return types, based on passed in arguments, possibly transientParams. One thing that a lot of ORMs support is returning different shapes of objects, based on passed in arguments. For example prisma only returns scalar values from the database by default, but will do database joins and nest objects if given the include parameter.

const user = await prisma.user.findUnique({
   where: {
     id: '1',  
   },
});

user.accountId // Value is set
user.account // Type error and undefined since this was not included

const userWithAccount = await prisma.user.findUnique({
   where: {
     id: '1',  
   },
   include: {
      account: true,
   }
}); 

user.account // No longer a typeerror

Is this something fishery would be interested in trying to support?

Question: how to have required params?

Thank you for a very useful library!

My objects have onCreate and creating database records, there are required columns, but from TypeScript perspective all parameters are optional because of DeepPartial.

How to override factory params type to make some of the fields required?

onBuild function

Is your feature request related to a problem? Please describe.

I use MobX-state-tree which has its own (synchronous) create function to lift a JS object in to a 'model', see here.

Describe the solution you'd like

An onBuild function in fishery which I could use to tell the factory about Mobx-state-tree's create function,
so I get back a Mobx-state-tree model and not a vanilla JS object.

Describe alternatives you've considered

My current workaround is to use factory to create object, then separately call create:

s1_ = precipSensorFactory.build({ site: "Test site"})
s1 = PrecipSensor.create(s1_)

Additional context

factory-girl has this:
https://github.com/simonexmachina/factory-girl#afterbuild-functionmodel-attrs-buildoptions

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.