@farming-labs/orm

Query API

The runtime API is model-based and fully typed from the schema.

Model clients

Given:

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

you get model clients such as:

orm.user;
orm.profile;
orm.session;
orm.account;

Each model client supports:

findUnique

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

Use findUnique when the query should resolve to at most one record, such as email, id, token, or another unique lookup.

That includes declared compound unique keys.

const account = await orm.account.findUnique({
  where: {
    provider: "github",
    accountId: "gh_ada",
  },
  select: {
    userId: true,
  },
});

For findUnique(...), the where object should match exactly one unique key:

findOne

const user = await orm.user.findOne({
  where: {
    emailVerified: true,
  },
  orderBy: {
    createdAt: "asc",
  },
  select: {
    id: true,
    email: true,
  },
});

findOne is the simple ergonomic name for "give me the first matching record". It is useful for library code that does not want to expose Prisma-flavored method names to its own consumers.

findFirst

const user = await orm.user.findFirst({
  where: { email: "ada@farminglabs.dev" },
  select: {
    id: true,
    email: true,
  },
});

Returns one typed record or null.

findFirst is still available for teams that prefer that naming. In the current runtime it behaves the same as findOne.

findMany

const sessions = await orm.session.findMany({
  where: {
    token: { contains: "session" },
  },
  orderBy: {
    expiresAt: "desc",
  },
  take: 10,
  skip: 0,
  select: {
    id: true,
    token: true,
    expiresAt: true,
  },
});

Returns an array of typed records.

count

const sessionCount = await orm.session.count({
  where: {
    userId: "user_1",
  },
});

Returns the number of matching rows without loading the full records.

create

const user = await orm.user.create({
  data: {
    name: "Grace Hopper",
    email: "grace@farminglabs.dev",
  },
  select: {
    id: true,
    email: true,
    createdAt: true,
  },
});

Generated/default fields are filled by the driver where supported. In the memory driver:

createMany

const createdSessions = await orm.session.createMany({
  data: [
    {
      userId: "user_1",
      token: "token-1",
      expiresAt: new Date("2026-01-01T00:00:00.000Z"),
    },
    {
      userId: "user_1",
      token: "token-2",
      expiresAt: new Date("2026-02-01T00:00:00.000Z"),
    },
  ],
  select: {
    token: true,
  },
});

Returns the created records, narrowed by select if provided.

update

const updated = await orm.user.update({
  where: { email: "grace@farminglabs.dev" },
  data: {
    emailVerified: true,
  },
  select: {
    id: true,
    emailVerified: true,
  },
});

Returns the updated record or null.

updateMany

const updatedCount = await orm.session.updateMany({
  where: {
    userId: "user_1",
  },
  data: {
    expiresAt: new Date("2027-01-01T00:00:00.000Z"),
  },
});

Returns the number of updated rows.

upsert

const session = await orm.session.upsert({
  where: {
    token: "session-token",
  },
  create: {
    userId: "user_1",
    token: "session-token",
    expiresAt: new Date("2027-01-01T00:00:00.000Z"),
  },
  update: {
    expiresAt: new Date("2027-01-01T00:00:00.000Z"),
  },
  select: {
    token: true,
    expiresAt: true,
  },
});

This is especially useful for auth-style operations such as rotating sessions, refreshing tokens, or syncing provider accounts.

It also supports compound unique keys:

const account = await orm.account.upsert({
  where: {
    provider: "github",
    accountId: "gh_ada",
  },
  create: {
    userId: "user_1",
    provider: "github",
    accountId: "gh_ada",
  },
  update: {
    userId: "user_2",
  },
  select: {
    provider: true,
    accountId: true,
    userId: true,
  },
});

Integer and JSON filters

Integer fields participate in the normal comparison operators:

const activeUsers = await orm.user.findMany({
  where: {
    loginCount: {
      gte: 2,
    },
  },
  select: {
    email: true,
    loginCount: true,
  },
});

JSON fields can be written and read like any other scalar, and equality filters can use the raw JSON value directly:

const matchingAccounts = await orm.account.findMany({
  where: {
    metadata: {
      plan: "oss",
      scopes: ["repo:read", "repo:write"],
      flags: {
        sync: true,
      },
    },
  },
  select: {
    provider: true,
    metadata: true,
  },
});

This is especially useful for auth and plugin data where you want a stable typed contract in library code, but the underlying app may be using Prisma, Drizzle, Kysely, direct SQL, MongoDB, or Mongoose.

For compound upserts:

Enum, bigint, and decimal values

Enums behave like constrained strings in the unified API:

const premiumUsers = await orm.user.findMany({
  where: {
    tier: "pro",
  },
  select: {
    email: true,
    tier: true,
  },
});

Bigints use real JavaScript bigint values and support the normal comparison operators:

const quotaUsers = await orm.user.findMany({
  where: {
    quota: {
      gte: 1024n,
    },
  },
  select: {
    email: true,
    quota: true,
  },
});

Decimals stay as strings so precision is preserved across runtimes:

const updatedAccount = await orm.account.update({
  where: {
    provider: "github",
    accountId: "gh_ada",
  },
  data: {
    balance: "19.95",
  },
  select: {
    balance: true,
  },
});

Decimal outputs are normalized strings, so values like "12.50" may reload as "12.5" after a round-trip.

delete

const deleted = await orm.session.delete({
  where: { token: "session-token" },
});

Returns the number of deleted rows.

deleteMany

const deletedCount = await orm.session.deleteMany({
  where: {
    userId: "user_1",
  },
});

Returns the number of deleted rows across the whole match set.

select and type inference

The select object is one of the most important parts of the runtime API.

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

That means the result type is narrowed to exactly the selected fields and relations.

Relation selection

Relations can be selected with:

Example

const user = await orm.user.findFirst({
  where: { email: "ada@farminglabs.dev" },
  select: {
    id: true,
    sessions: {
      where: {
        token: { contains: "session" },
      },
      orderBy: {
        expiresAt: "desc",
      },
      take: 5,
      select: {
        token: true,
        expiresAt: true,
      },
    },
  },
});

where filters

Shorthand equality

where: {
  email: "ada@farminglabs.dev";
}

Operator objects

where: {
  email: { eq: "ada@farminglabs.dev" },
  token: { contains: "session" },
}

Supported operators

For strings:

For comparable values such as strings and dates:

Logical operators

where: {
  AND: [{ emailVerified: true }],
  OR: [
    { name: { contains: "Ada" } },
    { email: { contains: "@farminglabs.dev" } },
  ],
  NOT: { email: { contains: "@spam.test" } },
}

Sorting and paging

const users = await orm.user.findMany({
  orderBy: {
    createdAt: "desc",
  },
  skip: 20,
  take: 20,
});

Transactions

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

  await tx.profile.create({
    data: {
      userId: user.id,
      bio: "Kernel and schema pragmatist.",
    },
  });
});

Batch workflows

const [user, sessionCount, accountCount] = await orm.batch([
  (tx) =>
    tx.user.findUnique({
      where: { id: "user_1" },
      select: {
        id: true,
        email: true,
      },
    }),
  (tx) =>
    tx.session.count({
      where: { userId: "user_1" },
    }),
  (tx) =>
    tx.account.count({
      where: { userId: "user_1" },
    }),
] as const);

batch is helpful when a library wants one grouped storage call but still wants to keep each internal query simple and typed.

Auth-style storage example

const normalizeEmail = (email: string) => email.trim().toLowerCase();

export function createAuthStore(orm: typeof authOrm) {
  return {
    findUserByEmail(email: string) {
      return orm.user.findUnique({
        where: {
          email: normalizeEmail(email),
        },
        select: {
          id: true,
          name: true,
          email: true,
          profile: {
            select: {
              bio: true,
            },
          },
          accounts: {
            select: {
              provider: true,
              accountId: true,
            },
          },
          sessions: {
            select: {
              token: true,
              expiresAt: true,
            },
            orderBy: {
              expiresAt: "desc",
            },
          },
        },
      });
    },

    createOAuthUser(input: { name: string; email: string; provider: string; accountId: string }) {
      return orm.transaction(async (tx) => {
        const user = await tx.user.create({
          data: {
            name: input.name,
            email: normalizeEmail(input.email),
          },
          select: {
            id: true,
            email: true,
          },
        });

        const account = await tx.account.create({
          data: {
            userId: user.id,
            provider: input.provider,
            accountId: input.accountId,
          },
          select: {
            provider: true,
            accountId: true,
          },
        });

        return { user, account };
      });
    },

    rotateSession(input: { userId: string; token: string; expiresAt: Date }) {
      return orm.session.upsert({
        where: {
          token: input.token,
        },
        create: {
          userId: input.userId,
          token: input.token,
          expiresAt: input.expiresAt,
        },
        update: {
          expiresAt: input.expiresAt,
        },
        select: {
          token: true,
          expiresAt: true,
        },
      });
    },

    invalidateUserSessions(userId: string) {
      return orm.session.deleteMany({
        where: {
          userId,
        },
      });
    },
  };
}

If the storage layer normalizes emails, normalize them on write as well as on lookup. A lookup-only toLowerCase() can miss legacy or mixed-case rows.

This is the main point of the runtime contract: an auth package can write its storage layer once and then plug that same logic into whichever runtime driver the app eventually uses.

Design goals of the query API