thoughtbot / fishery

A library for setting up JavaScript objects as test data
MIT License
877 stars 36 forks source link

Dynamic Object Types #61

Open jpulec opened 3 years ago

jpulec commented 3 years ago

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?

stevehanson commented 3 years ago

@jpulec thanks for bringing this up! I was just looking at Prisma's types a few weeks ago and agree that they provide a lot of flexibility. If we could find a way to incorporate something like that into Fishery, I think that would be great. I haven't explored this much yet beyond looking through Prisma's implementation and am not even sure yet what would be feasible, but I welcome any contributions or thoughts in this area!

Prisma's implementation is unique and complicated, as they dynamically create the types for each model and hide them in node_modules. Since the types are generated for each model, they're simpler than they might otherwise be, but they are still too complex for most TS users (including myself) to be able to easily understand (see below).

I see this feature being complex to implement, so I'm also open to other approaches for Fishery that are simpler but still generally achieve the same goal. Eg. multiple factories that are related to each other but with different return types (eg. userFactory and userWithPostsFactory).

I've included my notes from my recent Prisma research below. Thanks again for bringing this up!

Prisma research

// returns type "User & { posts: Post[] }"
const users = await prisma.user.findMany({ include: { posts: true } });

They achieve all this by generating custom types per DB model when you update the ORM schema file and placing the types in node_modules/.prisma/index.d.ts.

Here’s the typing for user.findMany:

findMany<T extends UserFindManyArgs>(
  args?: SelectSubset<T, UserFindManyArgs>
): CheckSelect<T, PrismaPromise<Array<User>>, PrismaPromise<Array<UserGetPayload<T>>>>

UserGetPayload is where the magic of returning the correct type based on the include or select args takes place:

  export type UserGetPayload<
    S extends boolean | null | undefined | UserArgs,
    U = keyof S
      > = S extends true
        ? User
    : S extends undefined
    ? never
    : S extends UserArgs | UserFindManyArgs
    ?'include' extends U
    ? User  & {
    [P in TrueKeys<S['include']>]: 
          P extends 'posts'
        ? Array < PostGetPayload<S['include'][P]>>  : never
  } 
    : 'select' extends U
    ? {
    [P in TrueKeys<S['select']>]: P extends keyof User ?User [P]
  : 
          P extends 'posts'
        ? Array < PostGetPayload<S['select'][P]>>  : never
  } 
    : User
  : User
stevehanson commented 2 years ago

I've started brainstorming a refactoring of this library that I think will provide flexibility to accomplish most of what you are proposing here. See https://github.com/thoughtbot/fishery/issues/71#issuecomment-998834975.