Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Need help with generated types #90

Open
Kshitiz-13 opened this issue Aug 15, 2023 · 7 comments
Open

Need help with generated types #90

Kshitiz-13 opened this issue Aug 15, 2023 · 7 comments
Labels
question Further information is requested

Comments

@Kshitiz-13
Copy link

Kshitiz-13 commented Aug 15, 2023

I am trying to use Kysely as a query builder and prisma for migration

Prisma Model


model User {
  id       Int      @id @default(autoincrement())
  name     String?
  email    String   @unique
  username String   @unique
  password String
  role     String   @default("USER")
  isActive Boolean  @default(true)
  profile  Profile?
}

The generated types are

export type User = {
  id: Generated<number>;
  name: string | null;
  email: string;
  username: string;
  password: string;
  role: Generated<string>;
  isActive: Generated<boolean>;
};

and for this query

export const findUserByEmail = async (email: string, username: string) => {
  const user = await db
    .selectFrom("User")
    .selectAll()
    .where((eb) => eb("email", "=", email).or("username", "=", username))
    .execute();

  return user[0];
};

user is of type

`const user: {
    password: string;
    id: number;
    name: string | null;
    email: string;
    username: string;
    role: string;
    isActive: boolean;
}[]`

So when I use the USER type form generated one

import { User } from "../db/types";

const createAuthToken = (user: User): string => {
  invariant(process.env.JWT_SECRET, "JWT_SECRET not set");

  return jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
    expiresIn: "90d",
  });
};

Here type mismatch is happening the returned user has no generated but the generated one has and I am not able to understand how to solve this

router.post("/signin", async (req, res) => {
  const { name, password, email, username } = req.body;

  const user = await findUserByEmail(email, username);

  if (!user) {
    return res.send("Email/password combination is invalid");
  }
  const validPassword = await bcrypt.compare(password, user.password);

  if (!validPassword) return res.json("Email/password combination is invalid");

  return res.send({ authToken: Auth.createAuthToken(user) });
});

Error message :
``

Argument of type '{ password: string; id: number; name: string | null; email: string; username: string; role: string; isActive: boolean; }' is not assignable to parameter of type 'User'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'ColumnType<number, number | undefined, number>'.ts(2345)
const user: {
password: string;
id: number;
name: string | null;
email: string;
username: string;
role: string;
isActive: boolean;
}

``

Screenshot 2023-08-15 at 3 45 26 PM

I am new to the codegen world any help would be really appreciated do I need to define types my self in this case?

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@cmmartin
Copy link

cmmartin commented Aug 16, 2023

Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'ColumnType<number, number | undefined, number>

along with...

Argument of type 'ColumnType<number, number | undefined, number>' is not assignable to parameter of type 'OperandValueExpressionOrList<DB, "..." | "...", "...">'

@mhaagens
Copy link

Having the same issue.

@IamFlowZ
Copy link

IamFlowZ commented Oct 4, 2023

Just using kysely and this project and run into the same issue. I think this stems from a combination of things

export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
  ? ColumnType<S, I | undefined, U>
  : ColumnType<T, T | undefined, T>;

Looking at the actual Generated type, if I'm reading this correctly, it's saying "if we have the select, insert and update types, than use those for the types in ColumnType, otherwise use the primitive type.

So the issue here, is that the columns wrapped by this Generated type are only passing their primitives, and not their Selectable, Insertable, Updatable counterparts.
So maybe the fix here is a series of "primitive" types that get generated like

export type selectableNum = Selectable<number>;
export type updateableNum = Updateable<number>; 

This leads to a separate issue (which I can open another thread if need be), in that the autogenerated types don't generate the Selectable, Insertable, Updatable interfaces that kysely basically requires. My project has found a workaround via the internet, but really feels like this lib should be responsible for those.

@arenddeboer
Copy link

arenddeboer commented Nov 18, 2023

Not sure where I've picked this up, but you can do the following to quickly reexport all types as selectable:

import { DB } from "kysely-codegen";
export type DBSelect = {
  [K in keyof DB]: Selectable<DB[K]>
}
// use it as:(If you have a table Media)
let myMediaItem : DBSelect['Media']

@jyrodrigues
Copy link

jyrodrigues commented Nov 24, 2023

I encountered the same problem. For anyone interested, here's the script I created for post-processing.

I'm running it with

# `./relative/path/to/types.d.ts` is relative from the directory where `postprocess.ts` is!
pushd path/to/script && bun postprocess.ts ./relative/path/to/types.d.ts && popd
// postprocess.ts

import { readFileSync, writeFileSync } from 'fs';
import * as path from 'path';

function transformFile() {
  const relativeFilePath = process.argv[2];

  if (!relativeFilePath) {
    console.error('No file path provided!');
    return;
  }

  const filePath = path.join(__dirname, relativeFilePath);

  const content = readFileSync(filePath, 'utf-8');
  const lines = content.split('\n');
  let dbInterfaceLineIndex = lines.findIndex((line) => {
    return line === 'export interface DB {';
  });

  if (dbInterfaceLineIndex === -1) {
    console.warn("Couldn't find `export interface DB {`! This script is fragile and the result is probably wrong!");
    dbInterfaceLineIndex = Number.MAX_SAFE_INTEGER; // So that the check below doesn't return true;
  }

  const transformedLines = lines.flatMap((line, index) => {
    if (line.startsWith('import type')) {
      //
      // Matches on `import type { ... } from "kysely";`
      // and appends `Selectable`, `Insertable` and `Updateable`
      // to whatever is inside `{ ... }`.
      //
      // E.g.
      //
      // import type { ColumnType } from "kysely";
      //
      // becomes
      //
      // import type { ColumnType, Insertable, Selectable, Updateable } from "kysely";
      //
      const importedTypes = line.match(/^import type \{(.*) \} from "kysely";$/)?.[1];
      if (!importedTypes) {
        return line;
      }

      return line.replace(importedTypes, `${importedTypes}, Insertable, Selectable, Updateable`);
    }

    if (line.startsWith('export interface')) {
      //
      // Matches on `export interface GeneratedInterface {`
      // appends `Table` to it: `GeneratedInterfaceTable`
      // and adds the three specific definitions:
      // `Selectable`, `Insertable` and `Updateable`
      //
      const interfaceName = line.match(/export interface (\w+) \{/)?.[1];
      if (!interfaceName || interfaceName === 'DB') {
        return line;
      }

      return [
        `export type ${interfaceName} = Selectable<${interfaceName}Table>;`,
        `export type New${interfaceName} = Insertable<${interfaceName}Table>;`,
        `export type ${interfaceName}Update = Updateable<${interfaceName}Table>;`,
        '',
        line.replace(interfaceName, `${interfaceName}Table`),
      ];
    }

    if (index > dbInterfaceLineIndex) {
      //
      // Matches table definitions in the `DB` interface like this
      //
      // export interface DB {
      //   user: User;
      // }
      //
      // and generate this
      //
      // export interface DB {
      //   user: UserTable;
      // }
      //
      const dbTableInterface = line.match(/^\s+\w+: (\w+);$/)?.[1];
      if (!dbTableInterface) {
        return line;
      }

      return line.replace(dbTableInterface, `${dbTableInterface}Table`);
    }

    return line;
  });

  writeFileSync(filePath, transformedLines.join('\n'), 'utf-8');
}

transformFile();

I'm using Postgres and simple tables for now, hopefully it works well in other contexts too :)

@RobinBlomberg RobinBlomberg added the question Further information is requested label Mar 7, 2024
@koskimas
Copy link
Contributor

koskimas commented Mar 24, 2024

I think it'd be beneficial for kysely-codegen to generate the Selectable, Insertable and Updateable interfaces too as people have said here. It's a very common issue that people try to use the table interfaces as row types, which one should never do.

So instead of

export interface User {
  id: Generated<string>
  ...
}

It'd be great if the generated types were actually

export interface UserTable {
  id: Generated<string>
  ...
}

export type User = Selectable<UserTable>
export type NewUser = Insertable<UserTable>
export type UserUpdate = Updateable<UserTable>

This is what we instruct people to do in Kysely docs.

@KaylaSolace
Copy link

KaylaSolace commented Apr 3, 2024

Agreed, for now I manually define the Selectable/Insertable/Updatable in my own file by having it generate stuff prefixed with the postgres schema name and then exporting stuff like User = Selectable<[schema name here]User>

I've accidentally used my table interfaces as row types multiple times already due to this not being the case by default, hahaha.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

9 participants