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
21.8k stars 496 forks source link

[BUG]: Unable to access RelationalQueryBuilder type #1319

Open mitchmcleod-ew opened 9 months ago

mitchmcleod-ew commented 9 months ago

What version of drizzle-orm are you using?

0.28.6

What version of drizzle-kit are you using?

No response

Describe the Bug

Attempting to import RelationalQueryBuilder to do generic relational querying. It appears that (at least for pg-core, possibly for other drivers as well) that the query-builders/query.d.ts declaration file isn't exported in that folders' barrel file and therefore cannot be imported.

Have also attempted to import from the file relatively but as is usually the case doing this most tsconfigs won't be setup to support it and ran into issues.

Expected behavior

No response

Environment & setup

No response

Angelelz commented 9 months ago

I've seen that in many cases the internal types might not be needed. Can you share an example where these types would help you construct a generic type that wouldn't be possible without them?

mitchmcleod-ew commented 9 months ago

I have a function that builds out standard queries across each of my tables with specific filters and (more crucially) external authorization logic applied to relational queries. I'm building this so that the query can be used by either a client directly or within a transaction hence I do not have the exact executing client instance until the time when the function is called.

For the parameters of the generic function I have managed to rebuild the type of the real findFirst and findMany parameters manually however this then completely divorces these types from the actual function params making them either a) not usable in a call to the real function due to inherent overrides of findFirst and findMany from different client types from or b) not type safe (if forcing use with real function with say any).

Trying to call these again gives a type error due to the inferred override from the union of different client types. For example here is an example of the query factory function with relevant types:

import {
  DBQueryConfig,
  ExtractTablesWithRelations,
  KnownKeysOnly,
  Table,
  TablesRelationalConfig,
} from 'drizzle-orm';
import { PgDatabase } from 'drizzle-orm/pg-core';

export type FindFirstArgs<
  T extends Table,
  FS extends Record<string, unknown> = Record<string, never>,
  S extends TablesRelationalConfig = ExtractTablesWithRelations<FS>,
> = KnownKeysOnly<
  any,
  Omit<DBQueryConfig<'many', true, S, S[T['_']['name']]>, 'limit'>
>;

export type FindManyArgs<
  T extends Table,
  FS extends Record<string, unknown> = Record<string, never>,
  S extends TablesRelationalConfig = ExtractTablesWithRelations<FS>,
> = KnownKeysOnly<any, DBQueryConfig<'many', true, S, S[T['_']['name']]>>;

export const createModelPgFindFirstQuery = <
  C extends PgDatabase<any, Exclude<FS, Record<string, never>>>,
  T extends Table,
  AM extends Record<string, any>,
  FM extends Record<string, any>,
  FS extends Record<string, unknown> = Record<string, never>,
>(
  tableRef: keyof C['query'],
  authzFn: (args: FindFirstArgs<T, FS>, meta?: AM) => Promise<boolean>,
  argsFilter?: (args: FindFirstArgs<T, FS>, meta?: FM) => FindFirstArgs<T, FS>,
) => {
  return async (
    dbClient: C,
    queryArgs: FindFirstArgs<T, FS>,
    authzMeta?: AM,
    filterMeta?: FM,
  ) => {
    // ... Apply filter
    const queryRef = dbClient.query[tableRef] as C['query'][typeof tableRef];
    const data = await queryRef.findFirst(queryArgs);
    // ... Authz logic
  };
};

There is also the situation where I want to return a PgRelationalQuery (which I don't need at the moment but will almost certainly need in the near future) I run into the dreaded The inferred typeof 'X' cannot be named without a reference to 'Y'. This is likely not portable. A type annotation is necessary. because the reference file is not exported.

Essentially while it may be possible to work around this for some cases it makes it unnecessarily difficult and in other cases I don't see how this is possible.

Is there an issue with having these types exposed? It appears that all other internal types in this folder are already exposed and it doesn't look like there are name conflicts.

Angelelz commented 9 months ago

Is there an issue with having these types exposed?

I don't think so, I'm just trying to understand the issue and use-case and maybe offer a workaround. I'll look into this later in the day and let you know.

Angelelz commented 9 months ago

Do the clients (or transaction objects) that you plan to pass to the resulting function have a different schema? I believe, you could start by not having the function be generic over the client as a first step in simplifying this. If you are interested, open a help thread in the discord server to discuss more and see if we can come up with a workaround.

If you assume the schema won't change, you can have the following factory function (No internal types needed, only TS magic):

import { AnyPgTable } from "drizzle-orm/pg-core";
import { db } from "./pg/pg";
import * as schema from "./pg/schema";
import { eq } from "drizzle-orm";

type ExtractTables<T extends Record<string, any>> = {
    [K in keyof T as T[K] extends AnyPgTable
        ? K
        : never]: T[K] extends AnyPgTable ? T[K] : never;
};

type DB = typeof db;

type Schema = typeof schema;

type TableName = keyof DB["query"];

type SchemaTables = ExtractTables<Schema>;

type FindFirstArg<T extends keyof SchemaTables> = Parameters<
    DB["query"][T]["findFirst"]
>[0];

export const createModelPgFindFirstQuery = <
    ttable extends TableName,
    AM extends Record<string, any>,
    FM extends Record<string, any>,
>(
    tableRef: ttable,
    _authzFn: (args: FindFirstArg<ttable>, meta?: AM) => Promise<boolean>,
    _argsFilter?: (
        args: FindFirstArg<ttable>,
        meta?: FM,
    ) => FindFirstArg<ttable>,
) => {
    return async (
        dbClient: DB,
        queryArgs?: FindFirstArg<ttable>,
        _authzMeta?: AM,
        _filterMeta?: FM,
    ) => {
        // ... Apply filter
        const queryRef = dbClient.query[tableRef];
        const data = await queryRef.findFirst(queryArgs);
        // ... Authz logic
        return data;
    };
};

const getFirstUser = createModelPgFindFirstQuery(
    "users",
    () => new Promise((resolve) => resolve(true)),
);

const firstUser = await getFirstUser(db, {
    where: eq(schema.users.id, 1),
    with: { posts: true },
});

console.log(firstUser);

This is kinda working but not really, I just had time to look at it briefly. It doesn't infer the post array on the final type, but it gets it in the runtime.

Angelelz commented 9 months ago

I was thinking I could make it happen, so I came up with the following:

import { db } from "./pg/pg";
import * as schema from "./pg/schema";
import { BuildQueryResult, ExtractTablesWithRelations, eq } from "drizzle-orm";

type DB = typeof db;

type Schema = typeof schema;

type TableNames = keyof DB["query"];

type FindFirstArg<T extends TableNames> = Parameters<
    DB["query"][T]["findFirst"]
>[0];

type TablesWithRelations = ExtractTablesWithRelations<Schema>;

type TableWithRelations<TTableName extends keyof TablesWithRelations> =
    TablesWithRelations[TTableName];

export const createModelPgFindFirstQuery = <
    TTableName extends TableNames,
    AM extends Record<string, any>,
    FM extends Record<string, any>,
>(
    tableRef: TTableName,
    _authzFn: (args: FindFirstArg<TTableName>, meta?: AM) => Promise<boolean>,
    _argsFilter?: (
        args: FindFirstArg<TTableName>,
        meta?: FM,
    ) => FindFirstArg<TTableName>,
) => {
    return async <QueryArgs extends FindFirstArg<TTableName>>(
        dbClient: DB,
        queryArgs?: QueryArgs,
        _authzMeta?: AM,
        _filterMeta?: FM,
    ): Promise<
        | BuildQueryResult<
                TablesWithRelations,
                TableWithRelations<TTableName>,
                QueryArgs extends undefined ? true : QueryArgs
          >
        | undefined
    > => {
        // ... Apply filter
        const queryRef = dbClient.query[tableRef];
        const data = await queryRef.findFirst(queryArgs);
        // ... Authz logic
        return data as any;
    };
};

const getFirstUser = createModelPgFindFirstQuery(
    "users",
    () => new Promise((resolve) => resolve(true)),
);

const firstUser = await getFirstUser(db, {
    where: eq(schema.users.id, 1),
    columns: { name: true },
    with: { posts: true },
});

console.log(firstUser);
//              ^?  {
//                    name: string;
//                    posts: {
//                        id: number;
//                        createdAt: Date | null;
//                        content: string;
//                        authorId: number;
//                   }[];
//                } | undefined

Completely type safe from top to bottom. I only had to use as any once due to how complicated this type is. Oh, and it works if you pass it a transaction object as well

mitchmcleod-ew commented 9 months ago

Thank you for taking at look at this. Unfortunately both of these rely on you having the actual DB type that you are going to use to execute the query which from my reply I won't have.

Just to clarify this code is an abstraction layer that will be placed in an internal library and may be executed against any schema from any of our services which are not only different schemas but also run on different machines. This way we can standardise operations such as resource authorization and handling of soft deletes (or other filters) easily and consistently across all projects. I have successfully managed to solve this for mutations and the standard select queries but would really really like to use the relational queries if possible as it makes front end querying and filtering so much simpler and more versatile.

I could in theory infer the type from the uninstantiated PgDatabase type via query, tableName, findFirst such as you have done above for the FindFirstArgs type but as I mentioned in my response this causes a The inferred typeof 'X' cannot be named without a reference to 'Y'. This is likely not portable. A type annotation is necessary. error as the type that you are inferring is not exported and so TS considers it 'not portable'.

If you believe this is not a supported use case let me know and I'll patch package, but in general I feel that all types (unless completely obscure or entirely derivable) should be exported to enable developers to make the most of a package.

Angelelz commented 9 months ago

Very interesting, I actually brought this topic to the drizzle team recently. I think this is something they's like to improve on.

Angelelz commented 7 months ago

Have you looked at this lately? I think in the last couple versions they fixed this?