drizzle-team / drizzle-orm

Headless TypeScript ORM with a head. Runs on Node, Bun and Deno. Lives on the Edge and yes, it's a JavaScript ORM too 😅
https://orm.drizzle.team
Apache License 2.0
24.78k stars 656 forks source link

[FEATURE]: Select with tRPC #2593

Closed oney closed 1 month ago

oney commented 4 months ago

Describe what you want

The idea comes from GraphQL. One of GraphQL’s advantages is that it allows the client to specify exactly which fields and relations they need in a query.

Using Drizzle with tRPC can essentially achieve the same functionality, but it's not standardized. Normally, we would define the inputs in tRPC procedures and try to map these inputs to Drizzle’s select configuration.

However, why not pass the Drizzle select configuration directly as the input and feed it into Drizzle? This approach would be very useful as it provides GraphQL-like capabilities, making it more performant and efficient for both users and developers.

Here’s an example of what I mean:

export const article = publicProcedure
  .input(
    z.object({
      findConfig: z.custom<Magic>((_) => true),
    }),
  )
  .query(({ ctx, input }) => ctx.db.query.Article.findFirst(input.findConfig));

function App() {
  const { data } = api.article.useQuery({
    findConfig: {
      columns: {
        id: true,
        title: true,
      },
      with: {
        comments: true,
      },
    },
  });
  data?.id;
  data?.createdAt; // TS error: Property 'createdAt' does not exist on type
  data?.comments;
}

Of course, we need to restrict the input values to ensure only columns and relations configs can be used. Otherwise, it would be like passing raw SQL, which can do anything.

Currently, tRPC doesn't support generic output based on input, but we can still annotate the output type like this:

I’m able to achieve this, but the generic typing becomes very complex. It's something like this:

import type * as schema from "@acme/db/schema";

type TSchema = ExtractTablesWithRelations<typeof schema>;
type TFields = TSchema["Article"];
type TSelection = Omit<DBQueryConfig<"many", true, TSchema, TFields>, "limit">;
type ArticleFindConfig = KnownKeysOnly<
  TSelection,
  Omit<DBQueryConfig<"many", true, TSchema, TFields>, "limit">
>;

export const article = publicProcedure
  .input(
    z.object({
      findConfig: z.custom<ArticleFindConfig>((_) => true),
    }),
  )
  .query(({ ctx, input }) => ctx.db.query.Article.findFirst(input.findConfig));

function useArticleQuery<
  TSchema extends ExtractTablesWithRelations<typeof schema>,
  TFields extends TSchema["Article"],
  TSelection extends Omit<
    DBQueryConfig<"many", true, TSchema, TFields>,
    "limit"
  >,
>(input: {
  findConfig?: KnownKeysOnly<
    TSelection,
    Omit<DBQueryConfig<"many", true, TSchema, TFields>, "limit">
  >;
}) {
  return api.article.useQuery<
    BuildQueryResult<TSchema, TFields, TSelection>,
    BuildQueryResult<TSchema, TFields, TSelection>
  >(input);
}

function App() {
  const { data } = useArticleQuery({
    findConfig: {
      columns: {
        id: true,
        title: true,
      },
      with: {
        comments: true,
      },
    },
  });
  data?.id;
  data?.comments;
}

My question is how can I get the select config directly from db.query.Article.findFirst and also infer the query result so I don't need to copy the generic type by myself?

L-Mario564 commented 1 month ago

It's quite unlikely that we provide support for tRPC-specific patterns like this one, it would be another thing for us to create and maintain and only tRPC users would benefit from this. You can make your own abstraction, like the ones you've provided, and also make use of other first-party packages like Drizzle Zod