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

Design signature of arbitrary factory parameters #252

Open
Quramy opened this issue Jan 11, 2024 · 2 comments
Open

Design signature of arbitrary factory parameters #252

Quramy opened this issue Jan 11, 2024 · 2 comments
Labels
enhancement New feature or request rfc

Comments

@Quramy
Copy link
Owner

Quramy commented Jan 11, 2024

What

Like transient attributes in factory_bot, I'd like to provide feature to define and use arbitrary parameters in fabbrica.

Background

See #244 .

Points

Signature of this feature should satisfy the followings:

  1. Types for additional user-defined parameters are inferred whenever possible.
  2. New signature should be backward-compat.

Proposal for API signature

Add HOF withExtraParameters to defineModelFactory fn:

declare function withExtraParameters<TExtraParams>(defaultExtraParams: TExtraParams) => (options: UserFactoryOptions) => UserFactoryInterface<TExtraParams, UserFactoryOptions>;

defineModelFactory works not only function but also object which provides the HOF.

import { defineUserFactory } from "./__generated__/fabbrica";

export async function seed() {
  const UserFactory = defineUserFactory.withExtraParameters({ loginCount: 0 })({
    defaultData: ({ seq, loginCount }) => {
      console.log(seq, loginCount);
      return {};
    },

    traits: {
      withLoginRecords: {
        data: ({ loginCount }) => {
          console.log(loginCount);
          return {};
        },
      },
    },
  });

  await UserFactory.build({ loginCount: 100 });
  await UserFactory.build(); // UserFactory provides default value defined `withExtraParameters`(i.e. 0) as loginCount
  await UserFactory.use("withLoginRecords").build({ loginCount: 100 });
}

If you want fully example, see https://github.com/Quramy/prisma-fabbrica/blob/feature/transient_params/packages/artifact-testing/fixtures/callback/transients.ts

Why default parameters ?

Factory guarantees extra parameters existence because of default value. So developer can refer the extra parameters at implementation of defaultData or traits. ( Inspired from createContext in React) .

And default parameters object also tells to factory what kind of type for extra parameters via type inference.

Why HOF ?

The major reason is to infer types of the extra parameters and to provide the inferred type to factory definition. I also attempted the following pattern, but I can't achieve it.

  const UserFactory = defineUserFactory({
    defaultExtraParams: { loginCount: 0 },
    defaultData: ({ seq, loginCount }) => {
      console.log(seq, loginCount);
      return {};
    },
  })

Why named extraParameters ?

I think a different name would be fine. For example, transientFields or contextParameters.

Caveat

  • Developer can't define trait specified parameters. If a parameter is referred from one trait impl, it should be defined and provided the default value at the factory level
@Quramy Quramy added the enhancement New feature or request label Jan 11, 2024
@Quramy
Copy link
Owner Author

Quramy commented Jan 11, 2024

It seems so tough to allow like the following:

type Context = {
  hoge: boolean;
};

const UserFactory = defineUserFactory<Context>({
  defaultData: async (_, { hoge }) => {
    return {
      someRelated: await OtherFactory.build({ hoge }),
    };
  },
});

await UserFactory.create({ hoge: true });

Because of

// AS-IS
declare function defineUserFactory<TOptions extends UserFactoryDefineOptions>(
  options: TOptions
): UserFactoryInterface<TOptions>;

// Invalid type declaration because required type parameter TOptions isn't allowed after optional type parameter TContext
declare function defineUserFactory<
  TContext = {},
  TOptions extends UserFactoryDefineOptions
>(options: TOptions): UserFactoryInterface<TContext, TOptions>;

// The following is valid declaration. But defineUserFactory<Context> means "Set `Context` as type parameter `TOptions`(not `TContext`)".
declare function defineUserFactory<
  TOptions extends UserFactoryDefineOptions,
  TContext = {}
>(options: TOptions): UserFactoryInterface<TContext, TOptions>;

// If signature was overloaded and TContext was set as required, developer couldn't call defineUserFactory<Context> because lacking `TOption`.
declare function defineUserFactory<
  TContext,
  TOptions extends UserFactoryDefineOptions
>(options: TOptions): UserFactoryInterface<TContext, TOptions>;
declare function defineUserFactory<
  TOptions extends UserFactoryDefineOptions
>(options: TOptions): UserFactoryInterface<{}, TOptions>;

// The following signature is valid, but trait keys are inferred as `never`.
declare function defineUserFactory<
  TContext,
  TOptions extends UserFactoryDefineOptions = UserFactoryDefineOptions
>(options: TOptions): UserFactoryInterface<TContext, TOptions>;
declare function defineUserFactory<
  TOptions extends UserFactoryDefineOptions
>(options: TOptions): UserFactoryInterface<{}, TOptions>;

@subvertallchris
Copy link

This could work! I worry that defining all of the default values when also defining the factory could make it hard to maintain. For example, with a lot extra parameters, you are likely to have a few that need to be used together, or a few that are required for some traits to work correctly. Does the implementation get any easier if you push this down to the trait level?

withLoginRecords: {
        data: <T extends { loginCount: number }>({ loginCount }) => {
          console.log(loginCount);
          return {};
        },
      },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request rfc
Projects
None yet
Development

No branches or pull requests

2 participants