Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Providing a value for dependent objects #244

Open
subvertallchris opened this issue Dec 28, 2023 · 4 comments
Open

Providing a value for dependent objects #244

subvertallchris opened this issue Dec 28, 2023 · 4 comments

Comments

@subvertallchris
Copy link

Hello! First, I want to thank you so much for this library. I was so happy to find it and even happier to discover how well it's working and being maintained. I gave you and the project a shout out on Twitter, I hope you get more recognition for your work.

I'm curious to know what (if any) patterns exist for providing data to a factory for use in the building of its relations. For instance, image that objects in a project are scoped to an Organization. We want to ensure that as a chain of objects are created, they all get the same organizationId key associated with them. What I want to do is something like:

const organization = await OrganizationFactory.create();
const department = DepartmentFactory.provide({ organizationId: organization.organizationId }).use('withTeams').create();

In this example, withTeams might create a team, each team might create two offices, each office might create three desks. I each to use the organizationId if it is available.

Right now, I'm doing this manually. I'm using more narrowly scoped factories without traits then gluing them together. But there's a lot of boilerplate that I think the factory could provide. Does a pattern exist for this already, or is it something you'd be open to adding?

@Quramy
Copy link
Owner

Quramy commented Dec 29, 2023

I don't understand the role of provide you describe 🙇

Would it help if traits could define their arguments as well as names ?

const department = DepartmentFactory.use(['withTeams', { organizationId: organization.organizationId }]).create();

or , Maybe you're describing feature like https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#transient-attributes ?

@subvertallchris
Copy link
Author

subvertallchris commented Dec 29, 2023

I think that "if traits could define the arguments as well as names" would get the job done, yes! You might want to consider a syntax like .use([{ withTeams: { organizationId: organization.organizationId} }] so one could include multiple traits the way they can now.

Here is an example copy/paste from my work for more background. All of the data here came from Factories but we have to associate records back to their parents in a higher level function.

export const buyableDistroOrderFactory = async (orderId: string) => {
  const distro = await getDistroFactory().create();

  const distroOrder = await DistroOrderFactory.create({
    distro: {
      connect: {
        id: distro.id,
      },
    },
    order: {
      connect: {
        id: orderId,
      },
    },
  });

  const orderItems = await Promise.all(
    [0, 1, 2].map(() =>
      getOrderItemFactory()
         // the getter function returns `OrderItemFactoryInterface` and it does not know about the `wearable` trait
        .use('wearable' as never)
        .create({
          distroOrder: {
            connect: {
              id: distroOrder.id,
            },
          },
          order: {
            connect: {
              id: orderId,
            },
          },
        }),
    ),
  );

  // the `merchItem` is associated with the order and we need to connect it to the distro
  await getPrismaClient().merchItem.updateMany({
    where: {
      variants: {
        some: {
          options: {
            some: {
              orderItems: {
                some: {
                  id: {
                    in: orderItems.map((oi) => oi.id),
                  },
                },
              },
            },
          },
        },
      },
    },
    data: {
      // We need to set the distro ID here.
      distroId: distro.id,
    },
  });

  return distroOrder;
};

I need to associate the orderId and distroId with objects that are nested deeply within the structure. They are related objects of related objects. I'd like to provide key/value pairs to a factory that it can choose to use in whatever way it needs, more than setting a value explicitly. If you did it at the factory level, it could maybe used like:

const SomeFactory = defineSomeFactory<{ distroId?: string, orderId?: string }>({
  traits: {
    inDistro: {
      data: async ({ provided: { distroId, orderId }) => {
        distro: getDistroFactory().use('withArtists').provided({ distroId, orderId }).build()
      }
    }
  }
};

const DistroFactory = defineDistroFactory<{ distroId?: string, orderId?: string }>({
  traits: {
    withArtists: {
      data: ({ provided: { distroId, orderId }}) => {
        return {
          artist: getArtistDistro().build({ distroId }),
          orders: getMerchDistro().build({ distroId, orderId })
        }
      }
    }
  }
});

And then the next factory receives those values. Think of it like a context provider/consumer in React if you use that. Global values that a factory function can use.

IMO a benefit of doing it at the factory level instead of trait is it might cut down on some duplication. But on the other hand, letting traits define their own arguments would let someone say "This trait MUST have this extra data in order to be created", so that might be better.

Thank you again for the time and work. I am happy to contribute to this library if possible!

@Quramy
Copy link
Owner

Quramy commented Jan 10, 2024

@subvertallchris
Sorry for my late reply and thanks for your detailed description. After all, it seems that your example of .provide is similar transient attributes in factory_bot .

IMO a benefit of doing it at the factory level instead of trait

for now, first I'll consider a feature that allows user to add arbitrary parameters at the factory level.

Do you use method chaining (i.e. provide(...).build() ) intentionally in your example? I think it's more simple to provide additional parameters as build arguments:

        distro: getDistroFactory().use('withArtists').build({ providing: { distroId, orderId } })

        // or 

        distro: getDistroFactory().use('withArtists').build({ distroId, orderId })

@Quramy
Copy link
Owner

Quramy commented Jan 11, 2024

@subvertallchris
I'm designing this feature in #252. Please give some feedback if possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants