@farming-labs/orm

Runtime

In Farming Labs ORM, runtime is the layer that executes the typed query API.

It is separate from:

The shape

create-orm.ts
import { createMemoryDriver, createOrm } from "@farming-labs/orm";
import { authSchema } from "./schema";

const orm = createOrm({
  schema: authSchema,
  driver: createMemoryDriver(),
});

That gives you typed model clients such as orm.user, orm.session, and orm.account.

It also exposes the attached runtime handle on orm.$driver. That gives app code access to the underlying instance that was passed into the driver:

driver-handle.ts
const orm = createOrm({
  schema: authSchema,
  driver: createPrismaDriver({ client: prisma }),
});

orm.$driver.kind; // "prisma"
orm.$driver.client; // the same PrismaClient instance
orm.$driver.capabilities.upsert; // "native" | "emulated" | "none"
orm.$driver.capabilities.returning.update; // whether update returns records in this ORM layer

The handle also exposes read-only capability metadata:

driver-capabilities.ts
const orm = createOrm({
  schema: authSchema,
  driver: createPgPoolDriver(pool),
});

orm.$driver.capabilities.supportsJSON; // true
orm.$driver.capabilities.supportsTransactions; // true
orm.$driver.capabilities.nativeRelationLoading; // "partial"
orm.$driver.capabilities.numericIds; // "none" | "manual" | "generated"
orm.$driver.capabilities.textMatching.equality; // "database-default" | "case-sensitive" | "case-insensitive"
orm.$driver.capabilities.nativeRelations.hasMany; // true | false

That metadata is derived from the driver, not passed in manually through createOrm(...). It is frozen read-only runtime metadata, so higher layers can inspect it safely without treating it as mutable app state.

The repo also verifies this against real local database-backed runtimes. The local SQL and Prisma integration suites assert the same capability metadata on orm.$driver and again inside real transaction scopes.

If you need to inspect a raw client before building a driver, the core package also exposes detectDatabaseRuntime(...):

detect-runtime.ts
import { detectDatabaseRuntime } from "@farming-labs/orm";

const detected = detectDatabaseRuntime(prisma);

detected?.kind; // "prisma"
detected?.dialect; // "postgres" | "mysql" | "sqlite" when detectable
detected?.source; // "client" | "db" | "connection" | "pool" | "database"
detected?.client; // the same instance you passed in

If detection fails, use the diagnostic report instead of guessing:

explain-runtime-detection.ts
import { inspectDatabaseRuntime } from "@farming-labs/orm";

const report = inspectDatabaseRuntime(client);

report.runtime; // detected runtime or null
report.summary; // human-readable summary
report.hint; // recommended next step or override
report.candidates; // heuristics that almost matched

If you want the same fallback behavior in one step, use @farming-labs/orm-runtime:

create-orm-from-runtime.ts
import { createOrmFromRuntime } from "@farming-labs/orm-runtime";

const orm = await createOrmFromRuntime({
  schema: authSchema,
  client: prisma,
});

orm.$driver.kind; // "prisma"
orm.$driver.client; // the same PrismaClient instance

That light root entrypoint keeps createOrm(...) itself explicit while giving higher-level integrations a clean “accept the raw client and normalize it” entrypoint.

The setup helpers now live on the Node-only subpath:

Import them from @farming-labs/orm-runtime/setup. They use the same runtime detection layer to prepare direct SQL, Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, EdgeDB / Gel, Neo4j, Cloudflare D1, Cloudflare KV, Redis, Supabase, MongoDB, Mongoose, Firestore, DynamoDB, and Unstorage runtimes without inventing a separate config surface.

There is also a dedicated guide for this flow:

For EdgeDB / Gel specifically, the runtime path is supported directly through the Gel SQL client, while schema management stays in the app's own Gel migration or SQL workflow.

The runtime also understands declared model-level compound unique keys. If a schema says account(provider, accountId) is unique, live drivers can use that same shape in findUnique(...) and upsert(...).

Numeric IDs are first-class now too. id({ type: "integer" }) gives you manual numeric IDs, while id({ type: "integer", generated: "increment" }) enables auto-generated numeric IDs on the supporting SQL-family, Prisma, and memory runtimes.

Query surface

The runtime client supports:

Relations

All live runtime drivers support:

That means nested queries like user.profile, user.sessions, and session.user work across the supported runtimes.

For loading strategy, the current runtime behaves like this:

That fallback behavior is mostly a current implementation boundary, not a statement that the backend cannot do more. PostgreSQL, MySQL, SQLite, Prisma, and MongoDB all have richer native relation-loading options available. This repo currently uses the native path for the safest direct branches first and keeps the shared resolver for the more complex shapes until those planners are added.

It also means unique lookups can be:

Capabilities and Errors

Every live ORM client exposes read-only capability metadata on orm.$driver.capabilities.

That includes:

Runtime failures are normalized too. Duplicate-key, foreign-key, missing-table, deadlock, and transaction-conflict failures are exposed as OrmError values through the unified ORM client, so higher-level integrations do not need to parse Prisma, SQL, EdgeDB, MongoDB, or Mongoose error formats separately.

Runtime-aware schema setup failures from @farming-labs/orm-runtime/setup are exposed separately as RuntimeSetupError, so bootstrap and push failures keep the stage, runtime kind, dialect, and underlying cause available.

Available drivers

Why this matters

Libraries can write one storage layer against createOrm(...) and let each app bring its own runtime.

find-user-by-email.ts
async function findUserByEmail(orm: typeof authOrm, email: string) {
  return orm.user.findUnique({
    where: { email },
    select: {
      id: true,
      email: true,
      sessions: {
        select: { token: true },
      },
    },
  });
}

That helper stays the same whether the app uses Prisma, Drizzle, TypeORM, Sequelize, EdgeDB / Gel, Cloudflare D1, Cloudflare KV, Redis, Supabase, direct SQL, Firestore, DynamoDB, Unstorage, MongoDB, or Mongoose.

The same is true for compound auth lookups:

find-account.ts
async function findAccount(orm: typeof authOrm, provider: string, accountId: string) {
  return orm.account.findUnique({
    where: {
      provider,
      accountId,
    },
  });
}

Local verification

pnpm test already includes these real integration suites. Use the commands below when you want to rerun the database-backed paths directly.

terminal
pnpm test:local
pnpm test:local:prisma
pnpm test:local:drizzle
pnpm test:local:sqlite
pnpm test:local:postgres
pnpm test:local:mysql
pnpm test:local:supabase
pnpm test:local:dynamodb
pnpm test:local:unstorage
pnpm test:local:mongodb

Continue