prisma-fabbrica
prisma-fabbrica copied to clipboard
Design signature of arbitrary factory parameters
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:
- Types for additional user-defined parameters are inferred whenever possible.
- 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 {};
},
},
I've implemented this feature via #326 and released it as v2.2.0 🚀