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
23.59k stars 579 forks source link

Implement infering table model with relations #695

Open dankochetov opened 1 year ago

dankochetov commented 1 year ago

Prisma API:

import { Prisma } from '@prisma/client'

// 1: Define a type that includes the relation to `Post`
const userWithPosts = Prisma.validator<Prisma.UserDefaultArgs>()({
  include: { posts: true },
})

// 2: Define a type that only contains a subset of the scalar fields
const userPersonalData = Prisma.validator<Prisma.UserDefaultArgs>()({
  select: { email: true, name: true },
})

// 3: This type will include a user and all their posts
type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
tanjunior commented 1 year ago

+1

rollemira commented 11 months ago

Please? 🙏

VladBrok commented 11 months ago

I found a workaround for this. For example, I have two tables: employee with fields "id" and "role_id" and role with field "id". I want to get a type of such query:

(
    await db
      .select()
      .from(employee)
      .where(eq(employee.id, id))
      .leftJoin(role, eq(employee.roleId, role.id))
)[0]

(i.e. an employee with their role). The type would be:

export type EmployeeWithRole = {
   [employee._.name]: typeof employee.$inferSelect;
   [role._.name]: typeof role.$inferSelect | null;
};
Hebilicious commented 9 months ago

For anyone looking for a workaround : https://github.com/drizzle-team/drizzle-orm/discussions/1483

Thanks @Angelelz 🙏🏽

BearToCode commented 9 months ago

For queries using with I made these simple helper types:

import type { BuildQueryResult, DBQueryConfig, ExtractTablesWithRelations } from 'drizzle-orm';
import * as schema from './path/to/schema';

type Schema = typeof schema;
type TSchema = ExtractTablesWithRelations<Schema>;

export type IncludeRelation<TableName extends keyof TSchema> = DBQueryConfig<
  'one' | 'many',
  boolean,
  TSchema,
  TSchema[TableName]
>['with'];

export type InferResultType<
  TableName extends keyof TSchema,
  With extends IncludeRelation<TableName> | undefined = undefined
> = BuildQueryResult<
  TSchema,
  TSchema[TableName],
  {
    with: With;
  }
>;

You can now do:

type UserWithPosts = InferResultType<'user', { posts: true }>
amjed-ali-k commented 8 months ago

For queries using with I made these simple helper types:

import type { BuildQueryResult, DBQueryConfig, ExtractTablesWithRelations } from 'drizzle-orm';
import * as schema from './path/to/schema';

type Schema = typeof schema;
type TSchema = ExtractTablesWithRelations<Schema>;

export type IncludeRelation<TableName extends keyof TSchema> = DBQueryConfig<
  'one' | 'many',
  boolean,
  TSchema,
  TSchema[TableName]
>['with'];

export type InferResultType<
  TableName extends keyof TSchema,
  With extends IncludeRelation<TableName> | undefined = undefined
> = BuildQueryResult<
  TSchema,
  TSchema[TableName],
  {
    with: With;
  }
>;

You can now do:

type UserWithPosts = InferResultType<'user', { posts: true }>

Thanks a lot ❤️

AyandaAde commented 8 months ago
import type { BuildQueryResult, DBQueryConfig, ExtractTablesWithRelations } from 'drizzle-orm';
import * as schema from './path/to/schema';

type Schema = typeof schema;
type TSchema = ExtractTablesWithRelations<Schema>;

export type IncludeRelation<TableName extends keyof TSchema> = DBQueryConfig<
  'one' | 'many',
  boolean,
  TSchema,
  TSchema[TableName]
>['with'];

export type InferResultType<
  TableName extends keyof TSchema,
  With extends IncludeRelation<TableName> | undefined = undefined
> = BuildQueryResult<
  TSchema,
  TSchema[TableName],
  {
    with: With;
  }
>;

can you do this but with a nested with?

Lukasz17git commented 8 months ago

You can also use the type BearToCode provided in https://github.com/drizzle-team/drizzle-orm/issues/695#issuecomment-1891811082 to achieve that:

type UserWithPostsWithUser = InferResultType<'user', { posts: { with: user: true } }>

I ll also post an alternative using unions if nested relations or filtering fields/columns aren't needed.

Somewhere in your app:

import type { db } from '@/path-to-db';

type DB = typeof db
type DbSchemas = NonNullable<DB['_']['schema']>

export type SchemaRelations<
   TTableName extends keyof DbSchemas,
   TExcludeRelations extends keyof NonNullable<DbSchemas[TTableName]['relations']> = never,
> = Exclude<keyof NonNullable<DbSchemas[TTableName]['relations']>, TExcludeRelations>

export type SchemaWithRelations<
   TTableName extends keyof DbSchemas,
   TInclude extends SchemaRelations<TTableName> = never
> = BuildQueryResult<DbSchemas, DbSchemas[TTableName], { with: { [Key in TInclude]: true } }>

In your schema.ts file:

(-) // export type User = typeof USERS_TABLE.$inferSelect
(+) export type User<T extends SchemaRelations<'USERS_TABLE'> = never> = SchemaWithRelations<'USERS_TABLE', T>
// OR if you want to exclude some relations, for example 'accounts' and 'sessions' from the User
(+) export type User<T extends SchemaRelations<'USERS_TABLE', 'accounts' | 'sessions'> = never> = SchemaWithRelations<'USERS_TABLE', T>

Usage:

type NormalUser = User
type UserWithReviews = User<'reviews'>
type UserWithReviewsRoomsEmployees = User<'reviews' | 'rooms' | 'employees'>
ax-at commented 7 months ago

For queries using with I made these simple helper types:

import type { BuildQueryResult, DBQueryConfig, ExtractTablesWithRelations } from 'drizzle-orm';
import * as schema from './path/to/schema';

type Schema = typeof schema;
type TSchema = ExtractTablesWithRelations<Schema>;

export type IncludeRelation<TableName extends keyof TSchema> = DBQueryConfig<
  'one' | 'many',
  boolean,
  TSchema,
  TSchema[TableName]
>['with'];

export type InferResultType<
  TableName extends keyof TSchema,
  With extends IncludeRelation<TableName> | undefined = undefined
> = BuildQueryResult<
  TSchema,
  TSchema[TableName],
  {
    with: With;
  }
>;

You can now do:

type UserWithPosts = InferResultType<'user', { posts: true }>

I upgraded it to include both columns and relations. As shown below:

import type {
  BuildQueryResult,
  DBQueryConfig,
  ExtractTablesWithRelations,
} from "drizzle-orm";

import type { schema } from "../schema";

type Schema = typeof schema;
type TablesWithRelations = ExtractTablesWithRelations<Schema>;

export type IncludeRelation<TableName extends keyof TablesWithRelations> =
  DBQueryConfig<
    "one" | "many",
    boolean,
    TablesWithRelations,
    TablesWithRelations[TableName]
  >["with"];

export type IncludeColumns<TableName extends keyof TablesWithRelations> =
  DBQueryConfig<
    "one" | "many",
    boolean,
    TablesWithRelations,
    TablesWithRelations[TableName]
  >["columns"];

export type InferQueryModel<
  TableName extends keyof TablesWithRelations,
  Columns extends IncludeColumns<TableName> | undefined = undefined,
  With extends IncludeRelation<TableName> | undefined = undefined,
> = BuildQueryResult<
  TablesWithRelations,
  TablesWithRelations[TableName],
  {
    columns: Columns;
    with: With;
  }
>;

You can now do:

type UserNameAndEmailWithPosts = InferQueryModel<'user', { name: true, email: true }, { posts: true }>
efstathiosntonas commented 7 months ago

@akashdevcc can you please post your InferResultType? Thanks

ax-at commented 6 months ago

@akashdevcc can you please post your InferResultType? Thanks

I now noticed, I made a mistake, instead of InferQueryModel, I used InferResultType. I have corrected it now.

@efstathiosntonas So now, do you want me to post the result of this statement?

type UserNameAndEmailWithPosts = InferQueryModel<'user', { name: true, email: true }, { posts: true }>
hlspablo commented 5 months ago

For queries using with I made these simple helper types:

import type { BuildQueryResult, DBQueryConfig, ExtractTablesWithRelations } from 'drizzle-orm';
import * as schema from './path/to/schema';

type Schema = typeof schema;
type TSchema = ExtractTablesWithRelations<Schema>;

export type IncludeRelation<TableName extends keyof TSchema> = DBQueryConfig<
  'one' | 'many',
  boolean,
  TSchema,
  TSchema[TableName]
>['with'];

export type InferResultType<
  TableName extends keyof TSchema,
  With extends IncludeRelation<TableName> | undefined = undefined
> = BuildQueryResult<
  TSchema,
  TSchema[TableName],
  {
    with: With;
  }
>;

You can now do:

type UserWithPosts = InferResultType<'user', { posts: true }>

I upgraded it to include both columns and relations. As shown below:

import type {
  BuildQueryResult,
  DBQueryConfig,
  ExtractTablesWithRelations,
} from "drizzle-orm";

import type { schema } from "../schema";

type Schema = typeof schema;
type TablesWithRelations = ExtractTablesWithRelations<Schema>;

export type IncludeRelation<TableName extends keyof TablesWithRelations> =
  DBQueryConfig<
    "one" | "many",
    boolean,
    TablesWithRelations,
    TablesWithRelations[TableName]
  >["with"];

export type IncludeColumns<TableName extends keyof TablesWithRelations> =
  DBQueryConfig<
    "one" | "many",
    boolean,
    TablesWithRelations,
    TablesWithRelations[TableName]
  >["columns"];

export type InferQueryModel<
  TableName extends keyof TablesWithRelations,
  Columns extends IncludeColumns<TableName> | undefined = undefined,
  With extends IncludeRelation<TableName> | undefined = undefined,
> = BuildQueryResult<
  TablesWithRelations,
  TablesWithRelations[TableName],
  {
    columns: Columns;
    with: With;
  }
>;

You can now do:

type UserNameAndEmailWithPosts = InferQueryModel<'user', { name: true, email: true }, { posts: true }>

this works, thank you

sillvva commented 5 months ago

You can also add column support like this:

import * as schema from "$server/db/schema";
import type { BuildQueryResult, DBQueryConfig, ExtractTablesWithRelations } from "drizzle-orm";

type TSchema = ExtractTablesWithRelations<typeof schema>;
type QueryConfig<TableName extends keyof TSchema> = DBQueryConfig<"one" | "many", boolean, TSchema, TSchema[TableName]>;

export type InferQueryModel<
    TableName extends keyof TSchema,
    QBConfig extends QueryConfig<TableName> = {}
> = BuildQueryResult<TSchema, TSchema[TableName], QBConfig>;
type Result = InferQueryModel<
  "logs",
  {
    columns: { id: true },
    with: {
      character: {
        columns: { id: true }
      }
    }
  }
>;

// type Result = { id: string; character: { id: string; } }
prichodko commented 5 months ago

Thanks for the help. It's useful!

Has anyone found a way to disallow passing non-existent columns?

For example:

type T = InferQueryModel<
  'Test',
  {
    id: true
    createdAt: true
    nonExistentKey: true // <- not erroring
  },
  {}
>

I noticed that drizzle uses a KnownKeysOnly helper but didn't find a way to make it work.

medv commented 4 months ago

Thanks for the help. It's useful!

Has anyone found a way to disallow passing non-existent columns?

For example:

type T = InferQueryModel<
  'Test',
  {
    id: true
    createdAt: true
    nonExistentKey: true // <- not erroring
  },
  {}
>

I noticed that drizzle uses a KnownKeysOnly helper but didn't find a way to make it work.

https://github.com/sindresorhus/type-fest/blob/main/source/exact.d.ts

prichodko commented 4 months ago

Thanks for the help. It's useful! Has anyone found a way to disallow passing non-existent columns? For example:

type T = InferQueryModel<
  'Test',
  {
    id: true
    createdAt: true
    nonExistentKey: true // <- not erroring
  },
  {}
>

I noticed that drizzle uses a KnownKeysOnly helper but didn't find a way to make it work.

https://github.com/sindresorhus/type-fest/blob/main/source/exact.d.ts

Thanks 🙏

Here is the full snippet that ensures that keys are always valid:


import type * as schema from './schema'
import type {
  BuildQueryResult,
  DBQueryConfig,
  ExtractTablesWithRelations,
} from 'drizzle-orm'
import type { Exact } from 'type-fest'

type TSchema = ExtractTablesWithRelations<typeof schema>

type QueryConfig<TableName extends keyof TSchema> = DBQueryConfig<
  'one' | 'many',
  boolean,
  TSchema,
  TSchema[TableName]
>

export type InferQueryModel<
  TableName extends keyof TSchema,
  QBConfig extends Exact<QueryConfig<TableName>, QBConfig> = {} // <-- notice Exact here
> = BuildQueryResult<TSchema, TSchema[TableName], QBConfig>

I also recommend using the Pretty TypeScript Errors extension that helps decipher the errors. 😅

alfredotoselli commented 3 months ago

I had this need for column definition of a TanStack Table, I used drizzle-zod to infer the types.

import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";

export const fooRelations = relations(fooTable, ({ one }) => ({
  bar: one(barTable, {
    fields: [fooTable.barId],
    references: [barTable.id],
  }),
}));

const selectBarSchema = createSelectSchema(barTable);
const selectFooSchema = createSelectSchema(fooTable);

const selectFooWithBarSchema = selectFooSchema.extend({
  user: selectFooSchema.nullable(),
});
export type selectFooWithBar = z.infer<typeof selectFooWithBarSchema >;
ludersGabriel commented 3 months ago

damn, was looking for something like that for an hour. Thanks @alfredotoselli !

geryruslandi commented 2 months ago

is there a way to make the relationship data to be optional ?

for example role and permissions

generated type would be { id: number; permissions?: {}[] }