├── .github ├── dependabot.yml └── workflows │ ├── check.yml │ ├── preview.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── biome.jsonc ├── docker-compose.yml ├── package.json ├── pnpm-lock.yaml ├── src ├── config.mts ├── connection.mts ├── dialect.mts ├── driver.mts ├── index.mts ├── isolation-levels.mts ├── kyselify.mts ├── supported-dialects.mts └── type-utils.mts ├── tests ├── nodejs │ ├── entity │ │ ├── Person.mts │ │ ├── Pet.mts │ │ └── Toy.mts │ ├── index.test.mts │ ├── scripts │ │ └── mysql-init.sql │ ├── test-setup.mts │ └── tsconfig.json └── scripts │ └── mysql-init.sql ├── tsconfig.base.json ├── tsconfig.json ├── tsup.config.mts ├── vitest.config.mts └── vitest.setup.mts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | reviewers: 13 | - "igalklebanov" 14 | versioning-strategy: increase 15 | groups: 16 | typescript-eslint: 17 | patterns: 18 | - "@typescript-eslint*" 19 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: checks 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | check: 11 | name: Checks 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | 21 | - name: Use Node.js 22 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 22 25 | cache: "pnpm" 26 | 27 | - name: Install dependencies 28 | run: pnpm i 29 | 30 | - name: Lint 31 | run: pnpm lint 32 | 33 | - name: Typecheck 34 | run: pnpm check:types 35 | 36 | - name: Build 37 | run: pnpm build 38 | 39 | - name: Exports 40 | run: pnpm check:exports 41 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: preview 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | paths-ignore: 9 | - ".github/workflows/check.yml" 10 | - ".github/workflows/test.yml" 11 | - ".gitignore" 12 | - "LICENSE" 13 | - "tests/**" 14 | - "tsconfig.json" 15 | - "tsup.*" 16 | - "vitest.*" 17 | - "*.jsonc" 18 | - "*.md" 19 | - "*.yml" 20 | 21 | jobs: 22 | release: 23 | name: Release preview build 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - name: Install pnpm 30 | uses: pnpm/action-setup@v4 31 | 32 | - name: Use Node.js 22 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: 22 36 | cache: "pnpm" 37 | 38 | - name: Install dependencies 39 | run: pnpm i 40 | 41 | - name: Build 42 | run: pnpm build 43 | 44 | - name: Release preview version 45 | run: pnpm release:preview 46 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | node: 11 | name: Node.js v${{ matrix.node-version }} 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | node-version: [20.x, 22.x, 23.x] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Run docker compose 25 | run: docker compose up -d 26 | 27 | - name: Install pnpm 28 | uses: pnpm/action-setup@v4 29 | 30 | - name: Install Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | cache: "pnpm" 34 | node-version: ${{ matrix.node-version }} 35 | 36 | - name: Install dependencies 37 | run: pnpm i 38 | 39 | - name: Build 40 | run: pnpm build 41 | 42 | - name: Test 43 | run: pnpm test 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | .attest 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kysely 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kysely-typeorm 2 | 3 | TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used with TypeScript and JavaScript (ES2021). Its goal is to always support the latest JavaScript features and provide additional features that help you to develop any kind of application that uses databases - from small applications with a few tables to large-scale enterprise applications with multiple databases. 4 | 5 | As of Mar 17, 2024, TypeORM [has 2,104,650 weekly downloads on npm](https://npmtrends.com/prisma-vs-sequelize-vs-typeorm) (2nd most popular ORM). It is a very popular ORM for Node.js and TypeScript. 6 | 7 | Just like most ORMs for Node.js, TypeORM has poor TypeScript support when it comes to writing queries outside the ORM's CRUD methods - something that happens more often than you might imagine - usually due to performance optimizations OR as a general escape hatch. This is where Kysely comes in. 8 | 9 | Kysely (pronounced “Key-Seh-Lee”) is a type-safe and autocompletion-friendly TypeScript SQL query builder. Inspired by Knex. Mainly developed for Node.js but also runs on Deno and in the browser. 10 | 11 | A match made in heaven, on paper. Let's see how it works in practice, with `kysely-typeorm` - a toolkit (dialect, type translators, etc.) that allows using your existing TypeORM setup with Kysely. 12 | 13 | ## Installation 14 | 15 | Main dependencies: 16 | 17 | ```sh 18 | npm i kysely kysely-typeorm typeorm 19 | ``` 20 | 21 | PostgreSQL: 22 | 23 | ```sh 24 | npm i pg 25 | ``` 26 | 27 | MySQL: 28 | 29 | ```sh 30 | npm i mysql2 31 | ``` 32 | 33 | MS SQL Server (MSSQL): 34 | 35 | > [!IMPORTANT] 36 | > While Kysely supports `tedious` with its core MS SQL Server (MSSQL) dialect, TypeORM uses `mssql` under the hood. This library doesn't use Kysely's own drivers. 37 | 38 | ```sh 39 | npm i mssql 40 | ``` 41 | 42 | SQLite: 43 | 44 | ```sh 45 | npm i better-sqlite3 46 | ``` 47 | 48 | ## Usage 49 | 50 | ### Entities & Types 51 | 52 | Update your entities using this library's `NonAttribute`, `Generated` and `GeneratedAlways` types. 53 | 54 | `src/entities/Person.ts`: 55 | 56 | ```diff 57 | +import type {Generated, JSONColumnType, NonAttribute, SimpleArray} from 'kysely-typeorm' 58 | import {BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn} from 'typeorm' 59 | import {PetEntity} from './Pet' 60 | 61 | @Entity({name: 'person'}) 62 | export class PersonEntity extends BaseEntity { 63 | @PrimaryGeneratedColumn() 64 | - id: number 65 | + id: Generated 66 | 67 | @Column({type: 'varchar', length: 255, nullable: true}) 68 | firstName: string | null 69 | 70 | @Column({type: 'varchar', length: 255, nullable: true}) 71 | middleName: string | null 72 | 73 | @Column({type: 'varchar', length: 255, nullable: true}) 74 | lastName: string | null 75 | 76 | @Column({type: 'varchar', length: 50}) 77 | gender: 'male' | 'female' | 'other' 78 | 79 | @Column({type: 'varchar', length: 50, nullable: true}) 80 | maritalStatus: 'single' | 'married' | 'divorced' | 'widowed' | null 81 | 82 | @Column({type: 'simple-array', nullable: true}) 83 | - listOfDemands: string[] | null 84 | + listOfDemands: SimpleArray 85 | 86 | @Column({type: 'simple-json', nullable: true}) 87 | - metadata: Record | null 88 | + metadata: JSONColumnType | null> 89 | 90 | @Column({type: 'jsonb', nullable: true}) 91 | - lastSession: { loggedInAt: string } | null 92 | + lastSession: JSONColumnType<{ loggedInAt: string } | null> 93 | 94 | @OneToMany(() => PetEntity, (pet) => pet.owner, {cascade: ['insert']}) 95 | - pets: PetEntity[] 96 | + pets: NonAttribute 97 | } 98 | ``` 99 | 100 | `src/entities/Pet.ts`: 101 | 102 | ```diff 103 | +import type {Generated, NonAttribute} from 'kysely-typeorm' 104 | import { 105 | BaseEntity, 106 | Column, 107 | Entity, 108 | Index, 109 | JoinColumn, 110 | ManyToOne, 111 | OneToMany, 112 | PrimaryGeneratedColumn, 113 | RelationId, 114 | } from 'typeorm' 115 | import {PersonEntity} from './Person' 116 | import {ToyEntity} from './Toy' 117 | 118 | @Entity({name: 'pet'}) 119 | export class PetEntity extends BaseEntity { 120 | @PrimaryGeneratedColumn() 121 | - id: number 122 | + id: Generated 123 | 124 | @Column({type: 'varchar', length: 255, unique: true}) 125 | name: string 126 | 127 | @ManyToOne(() => PersonEntity, (person) => person.pets, {onDelete: 'CASCADE'}) 128 | @JoinColumn({name: 'owner_id', referencedColumnName: 'id'}) 129 | @Index('pet_owner_id_index') 130 | - owner: PersonEntity 131 | + owner: NonAttribute 132 | 133 | @RelationId((pet: PetEntity) => pet.owner) 134 | ownerId: number 135 | 136 | @Column({type: 'varchar', length: 50}) 137 | species: 'dog' | 'cat' | 'hamster' 138 | 139 | @OneToMany(() => ToyEntity, (toy) => toy.pet, {cascade: ['insert']}) 140 | - toys: ToyEntity[] 141 | + toys: NonAttribute 142 | } 143 | ``` 144 | 145 | `src/entities/Toy.ts`: 146 | 147 | ```diff 148 | +import type {Generated, NonAttribute} from 'kysely-typeorm' 149 | import {BaseEntity, Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId} from 'typeorm' 150 | import {PetEntity} from './Pet' 151 | 152 | @Entity({name: 'toy'}) 153 | export class ToyEntity extends BaseEntity { 154 | @PrimaryGeneratedColumn({type: 'integer'}) 155 | - id: number 156 | + id: Generated 157 | 158 | @Column({type: 'varchar', length: 255, unique: true}) 159 | name: string 160 | 161 | @Column({type: 'double precision'}) 162 | price: number 163 | 164 | @ManyToOne(() => PetEntity, (pet) => pet.toys, {onDelete: 'CASCADE'}) 165 | @JoinColumn({name: 'pet_id', referencedColumnName: 'id'}) 166 | - pet: PetEntity 167 | + pet: NonAttribute 168 | 169 | @RelationId((toy: ToyEntity) => toy.pet) 170 | petId: number 171 | } 172 | ``` 173 | 174 | Translate your entities to Kysely table schema types via the `KyselifyEntity` helper type. 175 | 176 | `src/types/database.ts`: 177 | 178 | ```ts 179 | import type { KyselifyEntity } from "kysely-typeorm"; 180 | import type { PersonEntity } from "../entities/Person"; 181 | import type { PetEntity } from "../entities/Pet"; 182 | import type { ToyEntity } from "../entities/Toy"; 183 | 184 | export type PersonTable = KyselifyEntity; 185 | // ^? { id: Generated, firstName: string | null, ... } 186 | export type PetTable = KyselifyEntity; 187 | export type ToyTable = KyselifyEntity; 188 | 189 | export interface Database { 190 | person: PersonTable; 191 | pet: PetTable; 192 | toy: ToyTable; 193 | } 194 | ``` 195 | 196 | ### Kysely Instance 197 | 198 | Create a Kysely instance. Pass to it your existing TypeORM DataSource instance. 199 | 200 | `src/kysely.ts`: 201 | 202 | ```ts 203 | import { 204 | CamelCasePlugin, // optional 205 | Kysely, 206 | PostgresAdapter, 207 | PostgresIntrospector, 208 | PostgresQueryCompiler, 209 | } from "kysely"; 210 | import { KyselyTypeORMDialect } from "kysely-typeorm"; 211 | import type { Database } from "./types/database"; 212 | import { dataSource } from "./typeorm"; 213 | 214 | export const kysely = new Kysely({ 215 | dialect: new KyselyTypeORMDialect({ 216 | // kysely-typeorm also supports MySQL, MS SQL Server (MSSQL), and SQLite. 217 | kyselySubDialect: { 218 | createAdapter: () => new PostgresAdapter(), 219 | createIntrospector: (db) => new PostgresIntrospector(db), 220 | createQueryCompiler: () => new PostgresQueryCompiler(), 221 | }, 222 | typeORMDataSource: dataSource, 223 | }), 224 | // `CamelCasePlugin` is used to align with `typeorm-naming-strategies`'s `SnakeNamingStrategy`. 225 | plugins: [new CamelCasePlugin()], // optional 226 | }); 227 | ``` 228 | -------------------------------------------------------------------------------- /biome.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "ignore": ["package.json", "tsconfig*.json"] 5 | }, 6 | "javascript": { 7 | "formatter": { 8 | "quoteStyle": "single", 9 | "semicolons": "asNeeded" 10 | } 11 | }, 12 | "linter": { 13 | "rules": { 14 | "nursery": { 15 | "noCommonJs": "error", 16 | "useDeprecatedReason": "error" 17 | }, 18 | "style": { 19 | "noNonNullAssertion": "warn", 20 | "noParameterAssign": "warn" 21 | }, 22 | "suspicious": { 23 | "noAssignInExpressions": "off", 24 | "noConsole": "error", 25 | "noExplicitAny": "warn" 26 | } 27 | } 28 | }, 29 | "vcs": { 30 | "clientKind": "git", 31 | "enabled": true, 32 | "useIgnoreFile": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | # credit to Knex.js team for the following mssql setup here: 4 | mssql: 5 | image: mcr.microsoft.com/mssql/server:2022-latest 6 | ports: 7 | - "21433:1433" 8 | environment: 9 | ACCEPT_EULA: Y 10 | MSSQL_PID: Express 11 | SA_PASSWORD: KyselyTest0 12 | healthcheck: 13 | test: /opt/mssql-tools/bin/sqlcmd -S mssql -U sa -P 'KyselyTest0' -Q 'select 1' 14 | waitmssql: 15 | image: mcr.microsoft.com/mssql/server:2017-latest 16 | links: 17 | - mssql 18 | depends_on: 19 | - mssql 20 | environment: 21 | MSSQL_PID: Express 22 | entrypoint: 23 | - bash 24 | - -c 25 | # https://docs.microsoft.com/en-us/sql/relational-databases/logs/control-transaction-durability?view=sql-server-ver15#bkmk_DbControl 26 | - 'until /opt/mssql-tools/bin/sqlcmd -S mssql -U sa -P KyselyTest0 -d master -Q "CREATE DATABASE kysely_test; ALTER DATABASE kysely_test SET ALLOW_SNAPSHOT_ISOLATION ON; ALTER DATABASE kysely_test SET DELAYED_DURABILITY = FORCED"; do sleep 5; done' 27 | mysql: 28 | image: "mysql/mysql-server" 29 | environment: 30 | MYSQL_ROOT_PASSWORD: root 31 | MYSQL_DATABASE: kysely_test 32 | ports: 33 | - "3308:3306" 34 | volumes: 35 | - ./tests/scripts/mysql-init.sql:/data/application/init.sql 36 | command: --init-file /data/application/init.sql 37 | postgres: 38 | image: "postgres" 39 | environment: 40 | POSTGRES_DB: kysely_test 41 | POSTGRES_USER: kysely 42 | POSTGRES_HOST_AUTH_METHOD: trust 43 | ports: 44 | - "5434:5432" 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kysely-typeorm", 3 | "version": "0.3.0", 4 | "description": "Kysely dialect for TypeORM", 5 | "repository": "https://github.com/kysely-org/kysely-typeorm.git", 6 | "homepage": "https://github.com/kysely-org/kysely-typeorm", 7 | "author": "Igal Klebanov ", 8 | "license": "MIT", 9 | "main": "./dist/index.js", 10 | "module": "./dist/index.mjs", 11 | "types": "./dist/index.d.ts", 12 | "exports": { 13 | ".": { 14 | "import": { 15 | "types": "./dist/index.d.mts", 16 | "default": "./dist/index.mjs" 17 | }, 18 | "require": { 19 | "types": "./dist/index.d.ts", 20 | "default": "./dist/index.js" 21 | } 22 | } 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "keywords": [ 28 | "kysely", 29 | "typeorm", 30 | "postgres", 31 | "mysql", 32 | "postgresql", 33 | "mariadb", 34 | "sqlite", 35 | "better-sqlite3", 36 | "mssql", 37 | "dialect" 38 | ], 39 | "scripts": { 40 | "build": "tsup", 41 | "check:exports": "attw . --pack", 42 | "check:types": "tsc --noEmit", 43 | "lint": "biome ci", 44 | "prepublishOnly": "pnpm lint && pnpm build && pnpm check:exports", 45 | "release:preview": "pkg-pr-new publish", 46 | "start": "tsup --watch", 47 | "test": "vitest" 48 | }, 49 | "peerDependencies": { 50 | "kysely": ">= 0.24.0 < 1", 51 | "typeorm": ">= 0.3.0 < 0.4.0" 52 | }, 53 | "devDependencies": { 54 | "@arethetypeswrong/cli": "^0.18.1", 55 | "@arktype/attest": "^0.46.0", 56 | "@biomejs/biome": "^1.9.4", 57 | "@tsconfig/node22": "^22.0.2", 58 | "@types/lodash": "^4.17.17", 59 | "@types/node": "^22.15.30", 60 | "@types/pg": "^8.15.4", 61 | "better-sqlite3": "^11.10.0", 62 | "kysely": "^0.28.2", 63 | "lodash": "^4.17.21", 64 | "mssql": "^11.0.1", 65 | "mysql2": "^3.14.1", 66 | "pg": "^8.16.0", 67 | "pg-query-stream": "^4.10.0", 68 | "pkg-pr-new": "^0.0.51", 69 | "reflect-metadata": "^0.2.2", 70 | "sqlite3": "^5.1.7", 71 | "tsup": "^8.5.0", 72 | "typeorm": "^0.3.24", 73 | "typeorm-naming-strategies": "^4.1.0", 74 | "typescript": "^5.8.3", 75 | "vitest": "^3.2.2" 76 | }, 77 | "sideEffects": false, 78 | "packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677", 79 | "pnpm": { 80 | "onlyBuiltDependencies": [ 81 | "better-sqlite3", 82 | "sqlite3" 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/config.mts: -------------------------------------------------------------------------------- 1 | import type { Dialect } from 'kysely' 2 | import type { DataSource } from 'typeorm' 3 | 4 | export interface KyselyTypeORMDialectConfig { 5 | kyselySubDialect: KyselySubDialect 6 | typeORMDataSource: DataSource 7 | } 8 | 9 | export type KyselySubDialect = Omit 10 | -------------------------------------------------------------------------------- /src/connection.mts: -------------------------------------------------------------------------------- 1 | import type { 2 | CompiledQuery, 3 | DatabaseConnection, 4 | QueryResult, 5 | TransactionSettings, 6 | } from 'kysely' 7 | import type { QueryRunner } from 'typeorm' 8 | 9 | import { ISOLATION_LEVELS } from './isolation-levels.mjs' 10 | import { isObject } from './type-utils.mjs' 11 | 12 | export class KyselyTypeORMConnection implements DatabaseConnection { 13 | readonly #queryRunner: QueryRunner 14 | 15 | constructor(queryRunner: QueryRunner) { 16 | this.#queryRunner = queryRunner 17 | } 18 | 19 | async beginTransaction(settings: TransactionSettings): Promise { 20 | const { isolationLevel: kyselyIsolationLevel } = settings 21 | 22 | const isolationLevel = 23 | kyselyIsolationLevel && ISOLATION_LEVELS[kyselyIsolationLevel] 24 | 25 | if (isolationLevel === null) { 26 | throw new Error( 27 | `Isolation level '${kyselyIsolationLevel}' is not supported!`, 28 | ) 29 | } 30 | 31 | await this.#queryRunner.startTransaction(isolationLevel) 32 | } 33 | 34 | async commitTransaction(): Promise { 35 | await this.#queryRunner.commitTransaction() 36 | } 37 | 38 | async release(): Promise { 39 | await this.#queryRunner.release() 40 | } 41 | 42 | async rollbackTransaction(): Promise { 43 | await this.#queryRunner.rollbackTransaction() 44 | } 45 | 46 | async executeQuery( 47 | compiledQuery: CompiledQuery, 48 | ): Promise> { 49 | const result = await this.#queryRunner.query( 50 | compiledQuery.sql, 51 | [...compiledQuery.parameters], 52 | true, 53 | ) 54 | 55 | const { affected, raw, records } = result 56 | 57 | return { 58 | insertId: Number.isInteger(raw) 59 | ? BigInt(raw) 60 | : isObject(raw) && 'insertId' in raw && Number.isInteger(raw.insertId) 61 | ? BigInt(raw.insertId as number) 62 | : undefined, 63 | numAffectedRows: Number.isInteger(affected) 64 | ? // biome-ignore lint/style/noNonNullAssertion: it's alright. 65 | BigInt(affected!) 66 | : undefined, 67 | numChangedRows: 68 | isObject(raw) && 69 | 'changedRows' in raw && 70 | Number.isInteger(raw.changedRows) 71 | ? BigInt(raw.changedRows as number) 72 | : undefined, 73 | rows: records || [], 74 | } 75 | } 76 | 77 | async *streamQuery( 78 | compiledQuery: CompiledQuery, 79 | ): AsyncIterableIterator> { 80 | for await (const row of await this.#queryRunner.stream(compiledQuery.sql, [ 81 | ...compiledQuery.parameters, 82 | ])) { 83 | yield { rows: [row] } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/dialect.mts: -------------------------------------------------------------------------------- 1 | import type { 2 | DatabaseIntrospector, 3 | DefaultQueryCompiler, 4 | Dialect, 5 | DialectAdapter, 6 | Driver, 7 | Kysely, 8 | QueryCompiler, 9 | } from 'kysely' 10 | 11 | import type { KyselyTypeORMDialectConfig } from './config.mjs' 12 | import { KyselyTypeORMDriver } from './driver.mjs' 13 | import { assertSupportedDialect } from './supported-dialects.mjs' 14 | 15 | export class KyselyTypeORMDialect implements Dialect { 16 | readonly #config: KyselyTypeORMDialectConfig 17 | 18 | constructor(config: KyselyTypeORMDialectConfig) { 19 | assertSupportedDialect(config.typeORMDataSource.options.type) 20 | this.#config = config 21 | } 22 | 23 | createAdapter(): DialectAdapter { 24 | return this.#config.kyselySubDialect.createAdapter() 25 | } 26 | 27 | createDriver(): Driver { 28 | return new KyselyTypeORMDriver(this.#config) 29 | } 30 | 31 | // biome-ignore lint/suspicious/noExplicitAny: this is fine. 32 | createIntrospector(db: Kysely): DatabaseIntrospector { 33 | return this.#config.kyselySubDialect.createIntrospector(db) 34 | } 35 | 36 | createQueryCompiler(): QueryCompiler { 37 | const queryCompiler = this.#config.kyselySubDialect.createQueryCompiler() 38 | 39 | // `typeorm` uses `node-mssql` internally with zero-based variable names. 40 | if (this.#config.typeORMDataSource.options.type === 'mssql') { 41 | ;( 42 | queryCompiler as QueryCompiler & { 43 | getCurrentParameterPlaceholder(this: DefaultQueryCompiler): string 44 | } 45 | ).getCurrentParameterPlaceholder = function ( 46 | this: DefaultQueryCompiler, 47 | ): string { 48 | return `@${this.numParameters - 1}` 49 | } 50 | } 51 | 52 | return queryCompiler 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/driver.mts: -------------------------------------------------------------------------------- 1 | import type { DatabaseConnection, Driver, TransactionSettings } from 'kysely' 2 | 3 | import type { KyselyTypeORMDialectConfig } from './config.mjs' 4 | import { KyselyTypeORMConnection } from './connection.mjs' 5 | 6 | export class KyselyTypeORMDriver implements Driver { 7 | readonly #config: KyselyTypeORMDialectConfig 8 | 9 | constructor(config: KyselyTypeORMDialectConfig) { 10 | this.#config = config 11 | } 12 | 13 | async acquireConnection(): Promise { 14 | const queryRunner = this.#config.typeORMDataSource.createQueryRunner() 15 | 16 | await queryRunner.connect() 17 | 18 | return new KyselyTypeORMConnection(queryRunner) 19 | } 20 | 21 | async beginTransaction( 22 | connection: KyselyTypeORMConnection, 23 | settings: TransactionSettings, 24 | ): Promise { 25 | await connection.beginTransaction(settings) 26 | } 27 | 28 | async commitTransaction(connection: KyselyTypeORMConnection): Promise { 29 | await connection.commitTransaction() 30 | } 31 | 32 | async destroy(): Promise { 33 | if (this.#config.typeORMDataSource.isInitialized) { 34 | await this.#config.typeORMDataSource.destroy() 35 | } 36 | } 37 | 38 | async init(): Promise { 39 | if (!this.#config.typeORMDataSource.isInitialized) { 40 | await this.#config.typeORMDataSource.initialize() 41 | } 42 | } 43 | 44 | async releaseConnection(connection: KyselyTypeORMConnection): Promise { 45 | await connection.release() 46 | } 47 | 48 | async rollbackTransaction( 49 | connection: KyselyTypeORMConnection, 50 | ): Promise { 51 | await connection.rollbackTransaction() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/index.mts: -------------------------------------------------------------------------------- 1 | export * from './config.mjs' 2 | export * from './dialect.mjs' 3 | export * from './driver.mjs' 4 | export * from './kyselify.mjs' 5 | -------------------------------------------------------------------------------- /src/isolation-levels.mts: -------------------------------------------------------------------------------- 1 | import type { IsolationLevel as KyselyIsolationLevel } from 'kysely' 2 | import type { IsolationLevel as TypeORMIsolationLevel } from 'typeorm/driver/types/IsolationLevel.js' 3 | 4 | export const ISOLATION_LEVELS = { 5 | 'read committed': 'READ COMMITTED', 6 | 'read uncommitted': 'READ UNCOMMITTED', 7 | 'repeatable read': 'REPEATABLE READ', 8 | serializable: 'SERIALIZABLE', 9 | snapshot: null, 10 | } as const satisfies Record 11 | -------------------------------------------------------------------------------- /src/kyselify.mts: -------------------------------------------------------------------------------- 1 | import type { 2 | Generated as KyselyGenerated, 3 | GeneratedAlways as KyselyGeneratedAlways, 4 | JSONColumnType as KyselyJSONColumnType, 5 | } from 'kysely' 6 | 7 | /** 8 | * This is used to mark entity properties that have their values generated by TypeORM 9 | * or the database, so that {@link KyselifyEntity} can mark them as Kysely Generated. 10 | * 11 | * Kysely treats Generated properties as write-optional. 12 | * 13 | * Also see {@link GeneratedAlways} and {@link NonAttribute}. 14 | * 15 | * ### example 16 | * 17 | * ```ts 18 | * import type { Generated, GeneratedAlways, NonAttribute } from 'kysely-typeorm' 19 | * import { 20 | * Column, 21 | * CreateDateColumn, 22 | * DeleteDateColumn, 23 | * Entity, 24 | * Generated as TypeORMGenerated, 25 | * PrimaryGeneratedColumn, 26 | * UpdateDateColumn, 27 | * VersionColumn 28 | * } from 'typeorm' 29 | * 30 | * \@Entity({ name: 'user' }) 31 | * export class UserEntity { 32 | * \@PrimaryGeneratedColumn() 33 | * id: GeneratedAlways 34 | * 35 | * \@Column({ type: 'varchar', length: 255, unique: true }) 36 | * username: string 37 | * 38 | * \@Column({ type: 'varchar', length: 255, nullable: true }) 39 | * steamAccountId: string | null 40 | * 41 | * \@CreateDateColumn() 42 | * createdAt: Generated 43 | * 44 | * \@UpdateDateColumn() 45 | * updatedAt: Generated 46 | * 47 | * \@DeleteDateColumn() 48 | * deletedAt: Generated 49 | * 50 | * \@VersionColumn() 51 | * version: Generated 52 | * 53 | * \@Column() 54 | * \@TypeORMGenerated('uuid') 55 | * uuid: Generated 56 | * } 57 | * 58 | * type User = KyselifyEntity 59 | * // ^? { id: GeneratedAlways, username: string, steamAccountId: string | null, createdAt: Generated, updatedAt: Generated, deletedAt: Generated, version: Generated, uuid: Generated } 60 | * ``` 61 | */ 62 | export type Generated = 63 | | (Exclude & { 64 | readonly __kysely__generated__?: unique symbol 65 | }) 66 | | Extract 67 | 68 | /** 69 | * This is used to mark entity properties that have their values generated by TypeORM 70 | * or the database, so that {@link KyselifyEntity} can mark them as Kysely GeneratedAlways. 71 | * 72 | * Kysely treats GeneratedAlways properties as read-only. 73 | * 74 | * Also see {@link Generated} and {@link NonAttribute}. 75 | * 76 | * ### example 77 | * 78 | * ```ts 79 | * import type { Generated, GeneratedAlways, NonAttribute } from 'kysely-typeorm' 80 | * import { 81 | * Column, 82 | * CreateDateColumn, 83 | * DeleteDateColumn, 84 | * Generated as TypeORMGenerated, 85 | * PrimaryGeneratedColumn, 86 | * UpdateDateColumn, 87 | * VersionColumn 88 | * } from 'typeorm' 89 | * 90 | * \@Entity({ name: 'user' }) 91 | * export class UserEntity { 92 | * \@PrimaryGeneratedColumn() 93 | * id: GeneratedAlways 94 | * 95 | * \@Column({ type: 'varchar', length: 255, unique: true }) 96 | * username: string 97 | * 98 | * \@Column({ type: 'varchar', length: 255, nullable: true }) 99 | * steamAccountId: string | null 100 | * 101 | * \@CreateDateColumn() 102 | * createdAt: Generated 103 | * 104 | * \@UpdateDateColumn() 105 | * updatedAt: Generated 106 | * 107 | * \@DeleteDateColumn() 108 | * deletedAt: Generated 109 | * 110 | * \@VersionColumn() 111 | * version: Generated 112 | * 113 | * \@Column() 114 | * \@TypeORMGenerated('uuid') 115 | * uuid: Generated 116 | * } 117 | * 118 | * type User = KyselifyEntity 119 | * // ^? { id: GeneratedAlways, username: string, steamAccountId: string | null, createdAt: Generated, updatedAt: Generated, deletedAt: Generated, version: Generated, uuid: Generated } 120 | * ``` 121 | */ 122 | export type GeneratedAlways = 123 | | (Exclude & { 124 | readonly __kysely__generated__always__?: unique symbol 125 | }) 126 | | Extract 127 | 128 | /** 129 | * This is used to mark entity properties that are populated at runtime by TypeORM and do 130 | * not exist in the database schema, so that {@link KyselifyEntity} can exclude 131 | * them. 132 | * 133 | * ### example 134 | * 135 | * ```ts 136 | * import type { GeneratedAlways, NonAttribute } from 'kysely-typeorm' 137 | * import { 138 | * Column, 139 | * Entity, 140 | * JoinColumn, 141 | * ManyToMany, 142 | * ManyToOne, 143 | * OneToMany, 144 | * PrimaryGeneratedColumn, 145 | * RelationId, 146 | * VirtualColumn 147 | * } from 'typeorm' 148 | * import { ClanEntity } from './Clan' 149 | * import { PostEntity } from './Post' 150 | * import { RoleEntity } from './Role' 151 | * 152 | * \@Entity({ name: 'user' }) 153 | * export class UserEntity { 154 | * \@PrimaryGeneratedColumn() 155 | * id: GeneratedAlways 156 | * 157 | * \@Column({ type: 'varchar', length: 255, unique: true }) 158 | * username: string 159 | * 160 | * \@Column({ type: 'varchar', length: 255, nullable: true }) 161 | * steamAccountId: string | null 162 | * 163 | * \@OneToMany(() => PostEntity, (post) => post.user) 164 | * posts: NonAttribute 165 | * 166 | * \@ManyToOne(() => ClanEntity, (clan) => clan.users) 167 | * \@JoinColumn({ name: 'clanId', referencedColumnName: 'id' }) 168 | * clan: NonAttribute 169 | * 170 | * \@RelationId((user) => user.clan) 171 | * clanId: number | null 172 | * 173 | * \@ManyToMany(() => RoleEntity) 174 | * \@JoinTable() 175 | * roles: NonAttribute 176 | * 177 | * \@RelationId((role) => role.users) 178 | * roleIds: NonAttribute 179 | * 180 | * \@VirtualColumn({ query: (alias) => `select count("id") from "posts" where "author_id" = ${alias}.id` }) 181 | * totalPostsCount: NonAttribute 182 | * } 183 | * 184 | * type User = KyselifyEntity 185 | * // ^? { id: Generated, username: string, steamAccountId: string | null, clanId: number | null } 186 | * ``` 187 | */ 188 | export type NonAttribute = 189 | | (Exclude & { 190 | readonly __kysely__non__attribute__?: unique symbol 191 | }) 192 | | Extract 193 | 194 | /** 195 | * This is used to mark entity properties of type `'simple-array'` that are stored as 196 | * comma-separated string values in the database, and are transformed into arrays in the 197 | * TypeORM entity, so that they are properly represented as string values when 198 | * fetched from the database using Kysely. 199 | */ 200 | // biome-ignore lint/suspicious/noExplicitAny: this is fine. 201 | export type SimpleArray = 202 | | (Exclude & { 203 | readonly __kysely__simple__array__?: unique symbol 204 | }) 205 | | Extract 206 | 207 | /** 208 | * This is used to mark entity properties of type `'simple-json'` or other JSON types that are stored as 209 | * `JSON.stringify`d values in the database, and are `JSON.parse`d in the 210 | * TypeORM entity, so that they are properly represented as string values when 211 | * fetched from the database using Kysely. 212 | */ 213 | export type JSONColumnType = 214 | | (Exclude & { 215 | readonly __kysely__json__?: unique symbol 216 | }) 217 | | Extract 218 | 219 | /** 220 | * This is used to transform TypeORM entities into Kysely entities. 221 | * 222 | * Also see {@link Generated}, {@link GeneratedAlways} and {@link NonAttribute}. 223 | * 224 | * ### example 225 | * 226 | * ```ts 227 | * import type { Generated, GeneratedAlways, NonAttribute } from 'kysely-typeorm' 228 | * import { 229 | * Column, 230 | * CreateDateColumn, 231 | * DeleteDateColumn, 232 | * Entity, 233 | * Generated as TypeORMGenerated, 234 | * JoinColumn, 235 | * JoinTable, 236 | * ManyToMany, 237 | * ManyToOne, 238 | * OneToMany, 239 | * PrimaryGeneratedColumn, 240 | * RelationId, 241 | * UpdateDateColumn, 242 | * VersionColumn, 243 | * VirtualColumn 244 | * } from 'typeorm' 245 | * import { ClanEntity } from './Clan' 246 | * import { PostEntity } from './Post' 247 | * import { RoleEntity } from './Role' 248 | * 249 | * \@Entity({ name: 'user' }) 250 | * export class UserEntity { 251 | * \@PrimaryGeneratedColumn() 252 | * id: GeneratedAlways 253 | * 254 | * \@Column({ type: 'varchar', length: 255, unique: true }) 255 | * username: string 256 | * 257 | * \@Column({ type: 'varchar', length: 255, nullable: true }) 258 | * steamAccountId: string | null 259 | * 260 | * \@CreateDateColumn() 261 | * createdAt: Generated 262 | * 263 | * \@UpdateDateColumn() 264 | * updatedAt: Generated 265 | * 266 | * \@DeleteDateColumn() 267 | * deletedAt: Generated 268 | * 269 | * \@VersionColumn() 270 | * version: Generated 271 | * 272 | * \@Column() 273 | * \@TypeORMGenerated('uuid') 274 | * uuid: Generated 275 | * 276 | * \@OneToMany(() => PostEntity, (post) => post.user) 277 | * posts: NonAttribute 278 | * 279 | * \@ManyToOne(() => ClanEntity, (clan) => clan.users) 280 | * \@JoinColumn({ name: 'clanId', referencedColumnName: 'id' }) 281 | * clan: NonAttribute 282 | * 283 | * \@RelationId((user) => user.clan) 284 | * clanId: number | null 285 | * 286 | * \@ManyToMany(() => RoleEntity) 287 | * \@JoinTable() 288 | * roles: NonAttribute 289 | * 290 | * \@RelationId((role) => role.users) 291 | * roleIds: NonAttribute 292 | * 293 | * \@VirtualColumn({ query: (alias) => `select count("id") from "posts" where "author_id" = ${alias}.id` }) 294 | * totalPostsCount: NonAttribute 295 | * } 296 | * 297 | * export type User = KyselifyEntity 298 | * // ^? { id: GeneratedAlways, username: string, steamAccountId: string | null, createdAt: Generated, updatedAt: Generated, deletedAt: Generated, version: Generated, uuid: Generated, clandId: number | null } 299 | * ``` 300 | * 301 | * and then you can use it like this: 302 | * 303 | * ```ts 304 | * import { Clan } from './Clan' 305 | * import { Post } from './Post' 306 | * import { Role } from './Role' 307 | * import { User } from './User' 308 | * 309 | * export interface Database { 310 | * clan: Clan 311 | * post: Post 312 | * role: Role 313 | * user: User 314 | * } 315 | * 316 | * export const kysely = new Kysely( 317 | * // ... 318 | * ) 319 | * ``` 320 | */ 321 | export type KyselifyEntity = { 322 | // biome-ignore lint/suspicious/noExplicitAny: it's fine here. 323 | [K in keyof E as E[K] extends (...args: any) => any 324 | ? never 325 | : // Record 326 | string extends keyof NonNullable 327 | ? K 328 | : '__kysely__non__attribute__' extends keyof NonNullable 329 | ? never 330 | : K]-?: // Record 331 | string extends keyof NonNullable 332 | ? '__kysely__json__' extends keyof NonNullable 333 | ? E[K] extends JSONColumnType 334 | ? KyselyJSONColumnType 335 | : never 336 | : E[K] extends object | null 337 | ? KyselyJSONColumnType 338 | : never 339 | : '__kysely__generated__' extends keyof NonNullable 340 | ? E[K] extends Generated 341 | ? KyselyGenerated> 342 | : never 343 | : '__kysely__generated__always__' extends keyof NonNullable 344 | ? E[K] extends GeneratedAlways 345 | ? KyselyGeneratedAlways> 346 | : never 347 | : '__kysely__simple__array__' extends keyof NonNullable 348 | ? E[K] extends SimpleArray 349 | ? string | Extract 350 | : never 351 | : '__kysely__json__' extends keyof NonNullable 352 | ? E[K] extends JSONColumnType 353 | ? KyselyJSONColumnType 354 | : never 355 | : Exclude 356 | } 357 | -------------------------------------------------------------------------------- /src/supported-dialects.mts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm' 2 | 3 | export const SUPPORTED_DIALECTS = [ 4 | 'better-sqlite3', 5 | 'mssql', 6 | 'mysql', 7 | 'postgres', 8 | 'sqlite', 9 | ] as const satisfies DataSourceOptions['type'][] 10 | 11 | export type SupportedDialect = (typeof SUPPORTED_DIALECTS)[number] 12 | 13 | function isDialectSupported(dialect: string): dialect is SupportedDialect { 14 | return SUPPORTED_DIALECTS.includes(dialect as never) 15 | } 16 | 17 | export function assertSupportedDialect(dialect: string) { 18 | if (!isDialectSupported(dialect)) { 19 | throw new Error(`Unsupported dialect: ${dialect}!`) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/type-utils.mts: -------------------------------------------------------------------------------- 1 | export function isObject(thing: unknown): thing is Record { 2 | return typeof thing === 'object' && thing !== null && !Array.isArray(thing) 3 | } 4 | -------------------------------------------------------------------------------- /tests/nodejs/entity/Person.mts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | OneToMany, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm' 8 | import type { 9 | Generated, 10 | JSONColumnType, 11 | NonAttribute, 12 | SimpleArray, 13 | } from '../../../src/index.mjs' 14 | import { PetEntity } from './Pet.mjs' 15 | 16 | // Trying to recreate the following interface with typeorm: 17 | // 18 | // export interface Person { 19 | // id: Generated 20 | // first_name: string | null 21 | // middle_name: ColumnType 22 | // last_name: string | null 23 | // gender: 'male' | 'female' | 'other' 24 | // marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null 25 | // } 26 | // 27 | // .addColumn('first_name', 'varchar(255)') 28 | // .addColumn('middle_name', 'varchar(255)') 29 | // .addColumn('last_name', 'varchar(255)') 30 | // .addColumn('gender', 'varchar(50)', (col) => col.notNull()) 31 | // .addColumn('marital_status', 'varchar(50)') 32 | 33 | @Entity({ name: 'person' }) 34 | export class PersonEntity extends BaseEntity { 35 | @PrimaryGeneratedColumn() 36 | id: Generated 37 | 38 | @Column({ type: 'varchar', length: 255, nullable: true }) 39 | firstName: string | null 40 | 41 | @Column({ type: 'varchar', length: 255, nullable: true }) 42 | middleName: string | null 43 | 44 | @Column({ type: 'varchar', length: 255, nullable: true }) 45 | lastName: string | null 46 | 47 | @Column({ type: 'varchar', length: 50 }) 48 | gender: 'male' | 'female' | 'other' 49 | 50 | @Column({ type: 'varchar', length: 50, nullable: true }) 51 | maritalStatus: 'single' | 'married' | 'divorced' | 'widowed' | null 52 | 53 | @Column({ type: 'simple-json', nullable: true }) 54 | record: JSONColumnType | null> 55 | 56 | @Column({ type: 'simple-json', nullable: true }) 57 | obj: JSONColumnType<{ hello: 'world!' } | null> 58 | 59 | @Column({ type: 'simple-array', nullable: true }) 60 | listOfDemands: SimpleArray 61 | 62 | // @Column({ type: 'json', nullable: true }) 63 | // jason: Record | null 64 | 65 | @OneToMany( 66 | () => PetEntity, 67 | (pet) => pet.owner, 68 | { cascade: ['insert'] }, 69 | ) 70 | pets: NonAttribute 71 | } 72 | -------------------------------------------------------------------------------- /tests/nodejs/entity/Pet.mts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | Index, 6 | JoinColumn, 7 | ManyToOne, 8 | OneToMany, 9 | PrimaryGeneratedColumn, 10 | RelationId, 11 | } from 'typeorm' 12 | import type { Generated, NonAttribute } from '../../../src/index.mjs' 13 | import { PersonEntity } from './Person.mjs' 14 | import { ToyEntity } from './Toy.mjs' 15 | 16 | // Trying to recreate the following interface with typeorm: 17 | // 18 | // export interface Pet { 19 | // id: Generated 20 | // name: string 21 | // owner_id: number 22 | // species: 'dog' | 'cat' | 'hamster' 23 | // } 24 | // 25 | // .addColumn('name', 'varchar(255)', (col) => col.unique().notNull()) 26 | // .addColumn('owner_id', 'integer', (col) => col.references('person.id').onDelete('cascade').notNull()) 27 | // .addColumn('species', 'varchar(50)', (col) => col.notNull()) 28 | // 29 | // .createIndex('pet_owner_id_index').on('pet').column('owner_id') 30 | 31 | @Entity({ name: 'pet' }) 32 | export class PetEntity extends BaseEntity { 33 | @PrimaryGeneratedColumn() 34 | id: Generated 35 | 36 | @Column({ type: 'varchar', length: 255, unique: true }) 37 | name: string 38 | 39 | @ManyToOne( 40 | () => PersonEntity, 41 | (person) => person.pets, 42 | { onDelete: 'CASCADE' }, 43 | ) 44 | @JoinColumn({ name: 'owner_id', referencedColumnName: 'id' }) 45 | @Index('pet_owner_id_index') 46 | owner: NonAttribute 47 | 48 | @RelationId((pet: PetEntity) => pet.owner) 49 | ownerId: number 50 | 51 | @Column({ type: 'varchar', length: 50 }) 52 | species: 'dog' | 'cat' | 'hamster' 53 | 54 | @OneToMany( 55 | () => ToyEntity, 56 | (toy) => toy.pet, 57 | { cascade: ['insert'] }, 58 | ) 59 | toys: NonAttribute 60 | } 61 | -------------------------------------------------------------------------------- /tests/nodejs/entity/Toy.mts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseEntity, 3 | Column, 4 | Entity, 5 | JoinColumn, 6 | ManyToOne, 7 | PrimaryGeneratedColumn, 8 | RelationId, 9 | } from 'typeorm' 10 | import type { Generated, NonAttribute } from '../../../src/index.mjs' 11 | import { PetEntity } from './Pet.mjs' 12 | 13 | // Trying to recreate the following interface with typeorm: 14 | // 15 | // export interface Toy { 16 | // id: Generated 17 | // name: string 18 | // price: number 19 | // pet_id: number 20 | // } 21 | // 22 | // .addColumn('name', 'varchar(255)', (col) => col.unique().notNull()) 23 | // .addColumn('price', 'double precision', (col) => col.notNull()) 24 | // .addColumn('pet_id', 'integer', (col) => col.references('pet.id').onDelete('cascade').notNull()) 25 | 26 | @Entity({ name: 'toy' }) 27 | export class ToyEntity extends BaseEntity { 28 | @PrimaryGeneratedColumn({ type: 'integer' }) 29 | id: Generated 30 | 31 | @Column({ type: 'varchar', length: 255, unique: true }) 32 | name: string 33 | 34 | @Column({ type: 'double precision' }) 35 | price: number 36 | 37 | @ManyToOne( 38 | () => PetEntity, 39 | (pet) => pet.toys, 40 | { onDelete: 'CASCADE' }, 41 | ) 42 | @JoinColumn({ name: 'pet_id', referencedColumnName: 'id' }) 43 | pet: NonAttribute 44 | 45 | @RelationId((toy: ToyEntity) => toy.pet) 46 | petId: number 47 | } 48 | -------------------------------------------------------------------------------- /tests/nodejs/index.test.mts: -------------------------------------------------------------------------------- 1 | import { type DeleteResult, type InsertResult, UpdateResult, sql } from 'kysely' 2 | import { jsonArrayFrom as jsonArrayFromMssql } from 'kysely/helpers/mssql' 3 | import { jsonArrayFrom as jsonArrayFromMySQL } from 'kysely/helpers/mysql' 4 | import { jsonArrayFrom as jsonArrayFromPostgres } from 'kysely/helpers/postgres' 5 | import { jsonArrayFrom as jsonArrayFromSQLite } from 'kysely/helpers/sqlite' 6 | import { omit } from 'lodash' 7 | import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest' 8 | 9 | import { SUPPORTED_DIALECTS } from '../../src/supported-dialects.mjs' 10 | import { PersonEntity } from './entity/Person.mjs' 11 | import { 12 | DEFAULT_DATA_SET, 13 | type PerDialect, 14 | type TestContext, 15 | initTest, 16 | seedDatabase, 17 | } from './test-setup.mjs' 18 | 19 | for (const dialect of SUPPORTED_DIALECTS) { 20 | describe(`KyselyTypeORMDialect: ${dialect}`, () => { 21 | let ctx: TestContext 22 | 23 | const jsonArrayFrom = { 24 | 'better-sqlite3': jsonArrayFromSQLite, 25 | mssql: jsonArrayFromMssql, 26 | mysql: jsonArrayFromMySQL, 27 | postgres: jsonArrayFromPostgres, 28 | sqlite: jsonArrayFromSQLite, 29 | }[dialect] as typeof jsonArrayFromMySQL 30 | 31 | beforeEach(async () => { 32 | ctx = await initTest(dialect) 33 | await seedDatabase(ctx) 34 | }) 35 | 36 | afterEach(async () => { 37 | await ctx.typeORMDataSource.dropDatabase() 38 | }) 39 | 40 | afterAll(async () => { 41 | if (dialect === 'mysql') { 42 | setTimeout(() => process.exit(0), 1_000) 43 | } 44 | 45 | await ctx.kysely.destroy() 46 | }) 47 | 48 | it('should be able to perform select queries', async () => { 49 | const ormPeople = await PersonEntity.find({ 50 | select: { 51 | firstName: true, 52 | gender: true, 53 | lastName: true, 54 | maritalStatus: true, 55 | middleName: true, 56 | pets: { 57 | name: true, 58 | species: true, 59 | toys: true, 60 | }, 61 | listOfDemands: true, 62 | obj: true, 63 | record: true, 64 | // jason: true, 65 | }, 66 | relations: ['pets', 'pets.toys'], 67 | order: { id: 1 }, 68 | }) 69 | 70 | expect(ormPeople).to.have.lengthOf(DEFAULT_DATA_SET.length) 71 | 72 | const normalizedOrmPeople = ormPeople.map((ormPerson) => 73 | omit( 74 | { 75 | ...ormPerson, 76 | pets: ormPerson.pets.map((pet) => 77 | omit(pet, ['id', 'owner', 'ownerId']), 78 | ), 79 | // temporary, until we have a kysely plugin that can return array for these. 80 | listOfDemands: ormPerson.listOfDemands?.join(',') || null, 81 | }, 82 | ['id'], 83 | ), 84 | ) 85 | 86 | // expect(normalizedOrmPeople).to.deep.equal(DEFAULT_DATA_SET) 87 | 88 | const queryBuilderPeople = await ctx.kysely 89 | .selectFrom('person') 90 | .select((eb) => [ 91 | 'firstName', 92 | 'gender', 93 | 'lastName', 94 | 'maritalStatus', 95 | 'middleName', 96 | jsonArrayFrom( 97 | eb 98 | .selectFrom('pet') 99 | .whereRef('pet.ownerId', '=', 'person.id') 100 | .select(['pet.name', 'pet.species', sql`'[]'`.as('toys')]), 101 | ).as('pets'), 102 | 'record', 103 | 'obj', 104 | 'listOfDemands', 105 | // 'jason', 106 | ]) 107 | .execute() 108 | 109 | expect(queryBuilderPeople).to.deep.equal(normalizedOrmPeople) 110 | }) 111 | 112 | it('should be able to perform insert queries', async () => { 113 | const result = await ctx.kysely 114 | .insertInto('person') 115 | .values({ 116 | gender: 'female', 117 | listOfDemands: JSON.stringify(['crypto']), 118 | obj: JSON.stringify({ hello: 'world!' }), 119 | record: JSON.stringify({ key: 'value' }), 120 | // jason: JSON.stringify({ ok: 'bro' }), 121 | }) 122 | .executeTakeFirstOrThrow() 123 | 124 | expect(result).to.deep.equal( 125 | ( 126 | { 127 | 'better-sqlite3': { 128 | insertId: BigInt(DEFAULT_DATA_SET.length + 1), 129 | numInsertedOrUpdatedRows: BigInt(1), 130 | }, 131 | mssql: { insertId: undefined, numInsertedOrUpdatedRows: BigInt(1) }, 132 | mysql: { 133 | insertId: BigInt(DEFAULT_DATA_SET.length + 1), 134 | numInsertedOrUpdatedRows: BigInt(1), 135 | }, 136 | postgres: { 137 | insertId: undefined, 138 | numInsertedOrUpdatedRows: BigInt(1), 139 | }, 140 | sqlite: { 141 | insertId: undefined, 142 | numInsertedOrUpdatedRows: BigInt(0), 143 | }, 144 | } satisfies PerDialect<{ [K in keyof InsertResult]: InsertResult[K] }> 145 | )[dialect], 146 | ) 147 | }) 148 | 149 | if (dialect === 'postgres' || dialect === 'sqlite') { 150 | it('should be able to perform insert queries with returning', async () => { 151 | const result = await ctx.kysely 152 | .insertInto('person') 153 | .values({ 154 | gender: 'female', 155 | record: JSON.stringify({ key: 'value' }), 156 | listOfDemands: JSON.stringify(['crypto']), 157 | obj: JSON.stringify({ hello: 'world!' }), 158 | // jason: JSON.stringify({ ok: 'bro' }), 159 | }) 160 | .returning('id') 161 | .executeTakeFirst() 162 | 163 | expect(result).to.deep.equal({ id: DEFAULT_DATA_SET.length + 1 }) 164 | }) 165 | } 166 | 167 | it('should be able to perform update queries', async () => { 168 | const result = await ctx.kysely 169 | .updateTable('person') 170 | .set({ maritalStatus: 'widowed' }) 171 | .where('id', '=', 1) 172 | .executeTakeFirstOrThrow() 173 | 174 | expect(result).to.deep.equal( 175 | ( 176 | { 177 | 'better-sqlite3': new UpdateResult(BigInt(1), undefined), 178 | mssql: new UpdateResult(BigInt(1), undefined), 179 | mysql: new UpdateResult(BigInt(1), BigInt(1)), 180 | postgres: new UpdateResult(BigInt(1), undefined), 181 | sqlite: new UpdateResult(BigInt(0), undefined), 182 | } satisfies PerDialect<{ [K in keyof UpdateResult]: UpdateResult[K] }> 183 | )[dialect], 184 | ) 185 | }) 186 | 187 | if (dialect === 'postgres' || dialect === 'sqlite') { 188 | it('should be able to perform update queries with returning', async () => { 189 | const result = await ctx.kysely 190 | .updateTable('person') 191 | .set({ maritalStatus: 'widowed' }) 192 | .where('id', '=', 1) 193 | .returning(['gender']) 194 | .executeTakeFirstOrThrow() 195 | 196 | expect(result).to.deep.equal({ gender: DEFAULT_DATA_SET[0].gender }) 197 | }) 198 | } 199 | 200 | it('should be able to perform delete queries', async () => { 201 | const result = await ctx.kysely 202 | .deleteFrom('person') 203 | .where('id', '=', 1) 204 | .executeTakeFirstOrThrow() 205 | 206 | expect(result).to.deep.equal( 207 | ( 208 | { 209 | 'better-sqlite3': { numDeletedRows: BigInt(1) }, 210 | mssql: { numDeletedRows: BigInt(1) }, 211 | mysql: { numDeletedRows: BigInt(1) }, 212 | postgres: { numDeletedRows: BigInt(1) }, 213 | sqlite: { numDeletedRows: BigInt(0) }, 214 | } satisfies PerDialect<{ [K in keyof DeleteResult]: DeleteResult[K] }> 215 | )[dialect], 216 | ) 217 | }) 218 | 219 | if (dialect === 'postgres' || dialect === 'sqlite') { 220 | it('should be able to perform delete queries with returning', async () => { 221 | const result = await ctx.kysely 222 | .deleteFrom('person') 223 | .where('id', '=', 1) 224 | .returning('gender') 225 | .executeTakeFirstOrThrow() 226 | 227 | expect(result).to.deep.equal({ gender: DEFAULT_DATA_SET[0].gender }) 228 | }) 229 | } 230 | 231 | if (dialect === 'postgres' || dialect === 'mysql' || dialect === 'mssql') { 232 | it('should be able to stream query results', async () => { 233 | const people = [] 234 | 235 | for await (const person of ctx.kysely 236 | .selectFrom('person') 237 | .selectAll() 238 | .stream()) { 239 | people.push(person) 240 | } 241 | 242 | expect(people).to.have.lengthOf(DEFAULT_DATA_SET.length) 243 | expect(people).to.deep.equal( 244 | DEFAULT_DATA_SET.map((datum, index) => ({ 245 | id: index + 1, 246 | ...omit(datum, ['pets']), 247 | listOfDemands: datum.listOfDemands?.join(',') || null, 248 | })), 249 | ) 250 | }) 251 | } 252 | }) 253 | } 254 | -------------------------------------------------------------------------------- /tests/nodejs/scripts/mysql-init.sql: -------------------------------------------------------------------------------- 1 | CREATE USER 'kysely'@'%' IDENTIFIED WITH mysql_native_password BY 'kysely'; 2 | GRANT ALL ON *.* TO 'kysely'@'%'; 3 | CREATE DATABASE kysely_test; -------------------------------------------------------------------------------- /tests/nodejs/test-setup.mts: -------------------------------------------------------------------------------- 1 | import { 2 | CamelCasePlugin, 3 | Kysely, 4 | type KyselyPlugin, 5 | MssqlAdapter, 6 | MssqlIntrospector, 7 | MssqlQueryCompiler, 8 | MysqlAdapter, 9 | MysqlIntrospector, 10 | MysqlQueryCompiler, 11 | ParseJSONResultsPlugin, 12 | PostgresAdapter, 13 | PostgresIntrospector, 14 | PostgresQueryCompiler, 15 | SqliteAdapter, 16 | SqliteIntrospector, 17 | SqliteQueryCompiler, 18 | } from 'kysely' 19 | import 'reflect-metadata' 20 | import { DataSource, type DataSourceOptions, type DeepPartial } from 'typeorm' 21 | import { SnakeNamingStrategy } from 'typeorm-naming-strategies' 22 | 23 | import { 24 | type KyselifyEntity, 25 | type KyselySubDialect, 26 | KyselyTypeORMDialect, 27 | type KyselyTypeORMDialectConfig, 28 | } from '../../src/index.mjs' 29 | import type { SupportedDialect } from '../../src/supported-dialects.mjs' 30 | import { PersonEntity } from './entity/Person.mjs' 31 | import { PetEntity } from './entity/Pet.mjs' 32 | import { ToyEntity } from './entity/Toy.mjs' 33 | 34 | export type Person = KyselifyEntity 35 | export type Pet = KyselifyEntity 36 | export type Toy = KyselifyEntity 37 | 38 | export interface Database { 39 | person: Person 40 | pet: Pet 41 | toy: Toy 42 | } 43 | 44 | export interface TestContext { 45 | kysely: Kysely 46 | typeORMDataSource: DataSource 47 | } 48 | 49 | export type PerDialect = Record 50 | 51 | export const PLUGINS: KyselyPlugin[] = [ 52 | new ParseJSONResultsPlugin(), 53 | new CamelCasePlugin(), 54 | ] 55 | 56 | const POOL_SIZE = 10 57 | 58 | const sqliteSubDialect = { 59 | createAdapter: () => new SqliteAdapter(), 60 | createIntrospector: (db) => new SqliteIntrospector(db), 61 | createQueryCompiler: () => new SqliteQueryCompiler(), 62 | } satisfies KyselySubDialect 63 | 64 | const BASE_DATA_SOURCE_OPTIONS = { 65 | entities: [PersonEntity, PetEntity, ToyEntity], 66 | logging: false, 67 | namingStrategy: new SnakeNamingStrategy(), 68 | } satisfies Omit 69 | 70 | export const CONFIGS: PerDialect< 71 | Omit & { 72 | typeORMDataSourceOptions: DataSourceOptions 73 | } 74 | > = { 75 | 'better-sqlite3': { 76 | kyselySubDialect: sqliteSubDialect, 77 | typeORMDataSourceOptions: { 78 | ...BASE_DATA_SOURCE_OPTIONS, 79 | database: ':memory:', 80 | type: 'better-sqlite3', 81 | }, 82 | }, 83 | mssql: { 84 | kyselySubDialect: { 85 | createAdapter: () => new MssqlAdapter(), 86 | createIntrospector: (db) => new MssqlIntrospector(db), 87 | createQueryCompiler: () => new MssqlQueryCompiler(), 88 | }, 89 | typeORMDataSourceOptions: { 90 | ...BASE_DATA_SOURCE_OPTIONS, 91 | database: 'kysely_test', 92 | host: 'localhost', 93 | password: 'KyselyTest0', 94 | pool: { min: 0, max: POOL_SIZE }, 95 | port: 21433, 96 | type: 'mssql', 97 | username: 'sa', 98 | options: { 99 | trustServerCertificate: true, 100 | useUTC: true, 101 | }, 102 | }, 103 | }, 104 | mysql: { 105 | kyselySubDialect: { 106 | createAdapter: () => new MysqlAdapter(), 107 | createIntrospector: (db) => new MysqlIntrospector(db), 108 | createQueryCompiler: () => new MysqlQueryCompiler(), 109 | }, 110 | typeORMDataSourceOptions: { 111 | ...BASE_DATA_SOURCE_OPTIONS, 112 | bigNumberStrings: true, 113 | database: 'kysely_test', 114 | host: 'localhost', 115 | password: 'kysely', 116 | poolSize: POOL_SIZE, 117 | port: 3308, 118 | supportBigNumbers: true, 119 | type: 'mysql', 120 | username: 'kysely', 121 | }, 122 | }, 123 | postgres: { 124 | kyselySubDialect: { 125 | createAdapter: () => new PostgresAdapter(), 126 | createIntrospector: (db) => new PostgresIntrospector(db), 127 | createQueryCompiler: () => new PostgresQueryCompiler(), 128 | }, 129 | typeORMDataSourceOptions: { 130 | ...BASE_DATA_SOURCE_OPTIONS, 131 | database: 'kysely_test', 132 | host: 'localhost', 133 | poolSize: POOL_SIZE, 134 | port: 5434, 135 | type: 'postgres', 136 | username: 'kysely', 137 | useUTC: true, 138 | }, 139 | }, 140 | sqlite: { 141 | kyselySubDialect: sqliteSubDialect, 142 | typeORMDataSourceOptions: { 143 | ...BASE_DATA_SOURCE_OPTIONS, 144 | database: ':memory:', 145 | type: 'sqlite', 146 | }, 147 | }, 148 | } 149 | 150 | export async function initTest( 151 | dialect: SupportedDialect, 152 | ): Promise { 153 | const config = CONFIGS[dialect] 154 | 155 | const typeORMDataSource = new DataSource(config.typeORMDataSourceOptions) 156 | 157 | await typeORMDataSource.initialize() 158 | 159 | await typeORMDataSource.synchronize(true) 160 | 161 | const kysely = new Kysely({ 162 | dialect: new KyselyTypeORMDialect({ 163 | kyselySubDialect: config.kyselySubDialect, 164 | typeORMDataSource, 165 | }), 166 | plugins: PLUGINS, 167 | }) 168 | 169 | return { kysely, typeORMDataSource } 170 | } 171 | 172 | export const DEFAULT_DATA_SET = [ 173 | { 174 | firstName: 'Jennifer', 175 | middleName: null, 176 | lastName: 'Aniston', 177 | gender: 'female', 178 | pets: [{ name: 'Catto', species: 'cat', toys: [] }], 179 | maritalStatus: 'divorced', 180 | listOfDemands: ['money', 'power'], 181 | obj: { hello: 'world!' }, 182 | record: { key: 'value' }, 183 | // jason: { anotherKey: 1 }, 184 | }, 185 | { 186 | firstName: 'Arnold', 187 | middleName: null, 188 | lastName: 'Schwarzenegger', 189 | gender: 'male', 190 | pets: [{ name: 'Doggo', species: 'dog', toys: [] }], 191 | maritalStatus: 'divorced', 192 | listOfDemands: null, 193 | obj: null, 194 | record: null, 195 | // jason: null, 196 | }, 197 | { 198 | firstName: 'Sylvester', 199 | middleName: 'Rocky', 200 | lastName: 'Stallone', 201 | gender: 'male', 202 | pets: [{ name: 'Hammo', species: 'hamster', toys: [] }], 203 | maritalStatus: 'married', 204 | listOfDemands: ['money'], 205 | obj: { hello: 'world!' }, 206 | record: { anotherKey: 0 }, 207 | // jason: { nope: null }, 208 | }, 209 | ] as const satisfies DeepPartial[] 210 | 211 | export async function seedDatabase(_ctx: TestContext): Promise { 212 | for (const datum of DEFAULT_DATA_SET) { 213 | await PersonEntity.create(datum).save() 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /tests/nodejs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "noErrorTruncation": true, 6 | "strictPropertyInitialization": false, 7 | }, 8 | "include": ["**/*.mts"], 9 | } 10 | -------------------------------------------------------------------------------- /tests/scripts/mysql-init.sql: -------------------------------------------------------------------------------- 1 | CREATE USER 'kysely'@'%' IDENTIFIED WITH mysql_native_password BY 'kysely'; 2 | GRANT ALL ON *.* TO 'kysely'@'%'; 3 | CREATE DATABASE kysely_test; -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": {}, 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": {}, 4 | "exclude": ["node_modules", "dist"], 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /tsup.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | dts: true, 6 | entry: ['./src/index.mts'], 7 | format: ['cjs', 'esm'], 8 | shims: true, 9 | }) 10 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | allowOnly: false, 6 | globalSetup: ['./vitest.setup.mts'], 7 | typecheck: { 8 | enabled: true, 9 | ignoreSourceErrors: true, 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /vitest.setup.mts: -------------------------------------------------------------------------------- 1 | import { setup } from '@arktype/attest' 2 | 3 | export default () => setup({}) 4 | --------------------------------------------------------------------------------