Open mitchmcleod-ew opened 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?
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.
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.
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.
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
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.
Very interesting, I actually brought this topic to the drizzle team recently. I think this is something they's like to improve on.
Have you looked at this lately? I think in the last couple versions they fixed this?
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