Guid
Guid

Reputation: 2216

A generic repository with Kysely

I'm stuck with a generic issue concerning an attempt to write a generic repository with Kysely.

Example: TypeScript Playground

import { Kysely, Selectable } from "kysely";

export type FooTable = {
  uuid: string;
  name: string;
  fromFoo: boolean;
};

export type BarTable = {
  uuid: string;
  name: string;
  fromBar: string;
}

type Table = FooTable | BarTable;

type Tables = {
  foo: FooTable;
  bar: BarTable;
};

type Data = {
  uuid: string;
  name: string;
  [key: string]: unknown;
}

export interface GenericRepository<T extends Data> {
  findOne(id: Data["uuid"]): Promise<T | null>;
  /* other methods will follow (like deleteOne, etc) */
}

export function createGenericRepository<
  D extends Data,
  TableName extends keyof Tables,
  T extends Tables[TableName]
>({
  dataSource,
  tableName,
  toModel,
}: {
  dataSource: Kysely<Tables>;
  tableName: TableName;
  toModel: (data: Selectable<T>) => D;
}): GenericRepository<D> {
  return {
    async findOne(id) {
      const data = await dataSource
        .selectFrom(tableName)
        .selectAll()
        .where("uuid", "=", id)
        .executeTakeFirst();

      if (!data) {
        return null;
      }

      return toModel(data);
    }
  };
}

function createFooRepository(dataSource: Kysely<Tables>) {
  return createGenericRepository({
    dataSource,
    tableName: "foo",
    toModel: (data) => { return { uuid: "123", name: "name", fromFoo: true } }
  })
}

My issue is that type checking does not work as I was expected. At line 49 I have

Argument of type 'string' is not assignable to parameter of type 'OperandValueExpressionOrList<Tables, ExtractTableAlias<Tables, TableName>, "uuid">'.(2345).

I don't really understand what I could do to please typescript and kysely here.

Upvotes: 3

Views: 681

Answers (2)

German Gonzalez
German Gonzalez

Reputation: 311

I've been struggling with this for a couple of hours and I ended up doing something like this with the assumptions that all the tables will have a uuid: string column:

export interface GenericRepository<T extends Data> {
  findOne(id: T extends { id: infer U } ? U extends string ? U : never : never): Promise<T | null>
  /* other methods will follow (like deleteOne, etc) */
}

Typescript Playground

Upvotes: 0

pakut2
pakut2

Reputation: 672

This can be achieved using the ref method from Kysely Dynamic Module

As the documentation says:

This method is meant to be used in those cases where the column names come from the user input or are not otherwise known at compile time.

import { Kysely, Selectable } from "kysely";

export type FooTable = {
  uuid: string;
  name: string;
  fromFoo: boolean;
};

export type BarTable = {
  uuid: string;
  name: string;
  fromBar: string;
}

type Table = FooTable | BarTable;

type Tables = {
  foo: FooTable;
  bar: BarTable;
};

type Data = {
  uuid: string;
  name: string;
  [key: string]: unknown;
}

export interface GenericRepository<T extends Data> {
  findOne(id: Data["uuid"]): Promise<T | null>;
  /* other methods will follow (like deleteOne, etc) */
}

export function createGenericRepository<
  D extends Data,
  TableName extends keyof Tables,
  T extends Tables[TableName]
>({
  dataSource,
  tableName,
  toModel,
}: {
  dataSource: Kysely<Tables>;
  tableName: TableName;
  toModel: (data: Selectable<T>) => D;
}): GenericRepository<D> {
  return {
    async findOne(id) {
      const data = await dataSource
        .selectFrom(tableName)
        .selectAll()
        .where(dataSource.dynamic.ref("uuid"), "=", id)
        .executeTakeFirst();

      if (!data) {
        return null;
      }

      return toModel(data as Selectable<T>);
    }
  };
}

function createFooRepository(dataSource: Kysely<Tables>) {
  return createGenericRepository({
    dataSource,
    tableName: "foo",
    toModel: (data) => { return { uuid: "123", name: "name", fromFoo: true } }
  })
}

Typescript Playground

There's still the need to assert type when calling toModel callback, and type safety is lost when using dynamic.ref, so I imagine this solution can still be improved

Upvotes: 0

Related Questions