Quramy / prisma-fabbrica

Prisma generator to define model factory

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Design signature of arbitrary factory parameters

Quramy opened this issue · comments

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

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>;

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 {};
        },
      },