@farming-labs/orm

Kysely

Kysely integration in this repo is runtime-first.

Use @farming-labs/orm-kysely when:

Supported Kysely dialects

Runtime setup

import { createOrm } from "@farming-labs/orm";
import { createKyselyDriver } from "@farming-labs/orm-kysely";
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
import { authSchema } from "./schema";

const kysely = new Kysely({
  dialect: new PostgresDialect({
    pool: new Pool({
      connectionString: process.env.DATABASE_URL,
    }),
  }),
});

const orm = createOrm({
  schema: authSchema,
  driver: createKyselyDriver({
    db: kysely,
    dialect: "postgres",
  }),
});

From there, library code keeps using the unified API:

const user = await orm.user.findUnique({
  where: {
    email: "ada@farminglabs.dev",
  },
  select: {
    id: true,
    email: true,
    profile: {
      select: {
        bio: true,
      },
    },
    sessions: {
      orderBy: {
        token: "asc",
      },
      select: {
        token: true,
      },
    },
  },
});

What the Kysely driver is doing

The Kysely runtime package does not invent a second ORM API.

Instead it:

  1. receives the unified Farming ORM query
  2. executes through the app’s real Kysely database
  3. reuses the shared SQL runtime behavior for relation loading, filtering, sorting, paging, and mutation semantics

That shared SQL runtime now includes a native relation-loading path, so queries like session.user, session.user.profile, user.sessions, and simple explicit join-table manyToMany(...) traversal can load through one SQL statement. Relation branches with relation-level filters, paging, or ordering still fall back to the shared resolver.

That does not mean Kysely or the underlying database is missing join support. It only means the shared SQL runtime currently translates the direct singular subset natively and leaves the more complex branches on the fallback path for now.

That keeps the runtime behavior aligned with:

Relation support

The Kysely runtime covers:

That means auth-style lookups such as:

await orm.user.findUnique({
  where: { email: "ada@farminglabs.dev" },
  select: {
    id: true,
    profile: {
      select: { bio: true },
    },
    sessions: {
      select: { token: true },
    },
    organizations: {
      select: { name: true },
    },
  },
});

work the same way they do in the other live runtimes.

Mutations and transactions

The Kysely runtime also supports:

Example:

await orm.transaction(async (tx) => {
  const user = await tx.user.create({
    data: {
      email: "ada@farminglabs.dev",
      name: "Ada",
    },
    select: {
      id: true,
    },
  });

  await tx.session.upsert({
    where: {
      token: "session-token",
    },
    create: {
      userId: user.id,
      token: "session-token",
      expiresAt: new Date("2027-01-01T00:00:00.000Z"),
    },
    update: {
      expiresAt: new Date("2027-01-01T00:00:00.000Z"),
    },
  });
});

The same live Kysely matrix also verifies:

Local verification

The repo verifies Kysely locally against SQLite, PostgreSQL, and MySQL.

Run it with:

terminal
pnpm test:local:kysely

Helpful references

How it fits into a Kysely app

Typical flow:

  1. the library defines a reusable schema in @farming-labs/orm
  2. the app creates its own Kysely instance normally
  3. the app passes that instance to createKyselyDriver(...)
  4. the shared package keeps one storage/query layer for every supported stack

That is the main value: Kysely apps can participate in the same package-level storage contract as Prisma, Drizzle, SQL, and Mongo-backed apps.