From 4758f41a0278c07ff01eeb43c35699f798cc8010 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 24 Jul 2025 08:21:10 +0200 Subject: [PATCH] copy kysely helper types to better guarantee types --- app/src/adapter/index.ts | 4 +- app/src/core/config.ts | 2 +- app/src/core/types.ts | 191 ++++++++++++++++++++++ app/src/data/api/DataApi.ts | 3 +- app/src/data/entities/EntityTypescript.ts | 7 +- app/src/index.ts | 1 + app/tsconfig.json | 6 +- 7 files changed, 200 insertions(+), 14 deletions(-) diff --git a/app/src/adapter/index.ts b/app/src/adapter/index.ts index 3082d43f..9db6e756 100644 --- a/app/src/adapter/index.ts +++ b/app/src/adapter/index.ts @@ -68,7 +68,7 @@ export async function createAdapterApp m.default); diff --git a/app/src/core/config.ts b/app/src/core/config.ts index 99a90137..2b7fd776 100644 --- a/app/src/core/config.ts +++ b/app/src/core/config.ts @@ -1,7 +1,7 @@ /** * These are package global defaults. */ -import type { Generated } from "kysely"; +import type { Generated } from "./types"; export type PrimaryFieldType = IdType | Generated; diff --git a/app/src/core/types.ts b/app/src/core/types.ts index 1751766d..fd69d1cb 100644 --- a/app/src/core/types.ts +++ b/app/src/core/types.ts @@ -4,3 +4,194 @@ export interface Serializable { } export type MaybePromise = T | Promise; + +/** + * Types below are copied from kysely. + */ + +export type DrainOuterGeneric = [T] extends [unknown] ? T : never; +/** + * This type can be used to specify a different type for + * select, insert and update operations. + * + * Also see the {@link Generated} type. + * + * ### Examples + * + * The next example defines a number column that is optional + * in inserts and updates. All columns are always optional + * in updates so therefore we don't need to specify `undefined` + * for the update type. The type below is useful for all kinds of + * database generated columns like identifiers. The `Generated` + * type is actually just a shortcut for the type in this example: + * + * ```ts + * type GeneratedNumber = ColumnType + * ``` + * + * The above example makes the column optional in inserts + * and updates, but you can still choose to provide the + * column. If you want to prevent insertion/update you + * can se the type as `never`: + * + * ```ts + * type ReadonlyNumber = ColumnType + * ``` + * + * Here's one more example where the type is different + * for each different operation: + * + * ```ts + * type UnupdateableDate = ColumnType + * ``` + */ +export type ColumnType = { + readonly __select__: SelectType; + readonly __insert__: InsertType; + readonly __update__: UpdateType; +}; +/** + * A shortcut for defining database-generated columns. The type + * is the same for all selects, inserts and updates but the + * column is optional for inserts and updates. + * + * The update type is `S` instead of `S | undefined` because updates are always + * optional --> no need to specify optionality. + * ``` + */ +export type Generated = ColumnType; +/** + * A shortcut for defining columns that are only database-generated + * (like postgres GENERATED ALWAYS AS IDENTITY). No insert/update + * is allowed. + */ +export type GeneratedAlways = ColumnType; +/** + * A shortcut for defining JSON columns, which are by default inserted/updated + * as stringified JSON strings. + */ +export type JSONColumnType< + SelectType extends object | null, + InsertType = string, + UpdateType = string, +> = ColumnType; +/** + * Evaluates to `K` if `T` can be `null` or `undefined`. + */ +type IfNullable = undefined extends T ? K : null extends T ? K : never; +/** + * Evaluates to `K` if `T` can't be `null` or `undefined`. + */ +type IfNotNullable = undefined extends T + ? never + : null extends T + ? never + : T extends never + ? never + : K; +/** + * Evaluates to `K` if `T` isn't `never`. + */ +type IfNotNever = T extends never ? never : K; +export type SelectType = T extends ColumnType ? S : T; +export type InsertType = T extends ColumnType ? I : T; +export type UpdateType = T extends ColumnType ? U : T; +/** + * Keys of `R` whose `InsertType` values can be `null` or `undefined`. + */ +export type NullableInsertKeys = { + [K in keyof R]: IfNullable, K>; +}[keyof R]; +/** + * Keys of `R` whose `InsertType` values can't be `null` or `undefined`. + */ +export type NonNullableInsertKeys = { + [K in keyof R]: IfNotNullable, K>; +}[keyof R]; +/** + * Keys of `R` whose `SelectType` values are not `never` + */ +type NonNeverSelectKeys = { + [K in keyof R]: IfNotNever, K>; +}[keyof R]; +/** + * Keys of `R` whose `UpdateType` values are not `never` + */ +export type UpdateKeys = { + [K in keyof R]: IfNotNever, K>; +}[keyof R]; +/** + * Given a table interface, extracts the select type from all + * {@link ColumnType} types. + * + * ### Examples + * + * ```ts + * interface PersonTable { + * id: Generated + * first_name: string + * modified_at: ColumnType + * } + * + * type Person = Selectable + * // { + * // id: number, + * // first_name: string + * // modified_at: Date + * // } + * ``` + */ +export type Selectable = DrainOuterGeneric<{ + [K in NonNeverSelectKeys]: SelectType; +}>; +/** + * Given a table interface, extracts the insert type from all + * {@link ColumnType} types. + * + * ### Examples + * + * ```ts + * interface PersonTable { + * id: Generated + * first_name: string + * modified_at: ColumnType + * } + * + * type InsertablePerson = Insertable + * // { + * // id?: number, + * // first_name: string + * // modified_at: string + * // } + * ``` + */ +export type Insertable = DrainOuterGeneric< + { + [K in NonNullableInsertKeys]: InsertType; + } & { + [K in NullableInsertKeys]?: InsertType; + } +>; +/** + * Given a table interface, extracts the update type from all + * {@link ColumnType} types. + * + * ### Examples + * + * ```ts + * interface PersonTable { + * id: Generated + * first_name: string + * modified_at: ColumnType + * } + * + * type UpdateablePerson = Updateable + * // { + * // id?: number, + * // first_name?: string + * // } + * ``` + */ +export type Updateable = DrainOuterGeneric<{ + [K in UpdateKeys]?: UpdateType; +}>; diff --git a/app/src/data/api/DataApi.ts b/app/src/data/api/DataApi.ts index b4deb5d9..71b5f029 100644 --- a/app/src/data/api/DataApi.ts +++ b/app/src/data/api/DataApi.ts @@ -1,6 +1,5 @@ -import type { DB, EntityData, RepoQueryIn } from "bknd"; +import type { DB, EntityData, RepoQueryIn, Insertable, Selectable, Updateable } from "bknd"; -import type { Insertable, Selectable, Updateable } from "kysely"; import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules"; import type { FetchPromise, ResponseObject } from "modules/ModuleApi"; import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult"; diff --git a/app/src/data/entities/EntityTypescript.ts b/app/src/data/entities/EntityTypescript.ts index 79968fc7..82b0ea04 100644 --- a/app/src/data/entities/EntityTypescript.ts +++ b/app/src/data/entities/EntityTypescript.ts @@ -1,6 +1,6 @@ +import { autoFormatString } from "bknd/utils"; import type { Entity, EntityManager, TEntityType } from "data/entities"; import type { EntityRelation } from "data/relations"; -import { autoFormatString } from "core/utils"; import { usersFields } from "auth/auth-entities"; import { mediaFields } from "media/media-entities"; @@ -170,8 +170,7 @@ export class EntityTypescript { const strings: string[] = []; const tables: Record = {}; const imports: Record = { - "bknd/core": ["DB"], - kysely: ["Insertable", "Selectable", "Updateable", "Generated"], + bknd: ["DB", "Insertable", "Selectable", "Updateable", "Generated"], }; // add global types @@ -207,7 +206,7 @@ export class EntityTypescript { strings.push(tables_string); // merge - let merge = `declare module "bknd/core" {\n`; + let merge = `declare module "bknd" {\n`; for (const systemEntity of system_entities) { const system_fields = Object.keys(systemEntities[systemEntity.name]); const additional_fields = systemEntity.fields diff --git a/app/src/index.ts b/app/src/index.ts index 27061743..d42b76a9 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -51,6 +51,7 @@ export type { ListenerHandler, } from "core/events/EventListener"; export { EventManager, type EmitsEvents, type EventClass } from "core/events/EventManager"; +export type * from "core/types"; /** * Auth diff --git a/app/tsconfig.json b/app/tsconfig.json index a40d88a7..55264d43 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -32,12 +32,8 @@ "*": ["./src/*"], "bknd": ["./src/index.ts"], "bknd/utils": ["./src/core/utils/index.ts"], - "bknd/core": ["./src/core/index.ts"], "bknd/adapter": ["./src/adapter/index.ts"], - "bknd/client": ["./src/ui/client/index.ts"], - "bknd/data": ["./src/data/index.ts"], - "bknd/media": ["./src/media/index.ts"], - "bknd/auth": ["./src/auth/index.ts"] + "bknd/client": ["./src/ui/client/index.ts"] } }, "include": [