@farming-labs/orm

Unified Schema

The schema DSL is the center of @farming-labs/orm. Everything else either reads the schema directly or consumes its normalized manifest form.

Core building blocks

Full schema example

import {
  belongsTo,
  boolean,
  datetime,
  defineSchema,
  hasMany,
  hasOne,
  id,
  manyToMany,
  model,
  string,
} from "@farming-labs/orm";

export const appSchema = defineSchema({
  user: model({
    table: "users",
    description: "Primary application users.",
    fields: {
      id: id(),
      name: string(),
      email: string().unique().map("email_address").describe("Canonical login email"),
      emailVerified: boolean().default(false),
      createdAt: datetime().defaultNow(),
    },
    relations: {
      profile: hasOne("profile", { foreignKey: "userId" }),
      sessions: hasMany("session", { foreignKey: "userId" }),
      organizations: manyToMany("organization", {
        through: "membership",
        from: "userId",
        to: "organizationId",
      }),
    },
  }),

  profile: model({
    table: "profiles",
    fields: {
      id: id(),
      userId: string().unique().references("user.id"),
      bio: string().nullable(),
    },
    relations: {
      user: belongsTo("user", { foreignKey: "userId" }),
    },
  }),

  session: model({
    table: "sessions",
    fields: {
      id: id(),
      userId: string().references("user.id"),
      token: string().unique(),
      expiresAt: datetime(),
    },
    relations: {
      user: belongsTo("user", { foreignKey: "userId" }),
    },
  }),

  account: model({
    table: "accounts",
    fields: {
      id: id(),
      userId: string().references("user.id"),
      provider: string(),
      accountId: string(),
    },
    constraints: {
      unique: [["provider", "accountId"]],
      indexes: [["userId", "provider"]],
    },
    relations: {
      user: belongsTo("user", { foreignKey: "userId" }),
    },
  }),

  organization: model({
    table: "organizations",
    fields: {
      id: id(),
      slug: string().unique(),
      name: string(),
    },
  }),

  membership: model({
    table: "memberships",
    fields: {
      id: id(),
      userId: string().references("user.id"),
      organizationId: string().references("organization.id"),
      role: string(),
    },
  }),
});

Namespaced tables

Flat table names still work:

table: "users";

When you need a Postgres schema namespace, use tableName(...):

import { tableName } from "@farming-labs/orm";

user: model({
  table: tableName("users", { schema: "auth" }),
  fields: {
    id: id(),
    email: string().unique(),
  },
});

That keeps the logical model name as user while generators and the SQL runtime emit qualified identifiers like "auth"."users".

Use the structured helper instead of passing "auth.users" as a flat string. Schema-qualified strings are rejected so the ORM can keep table names and namespaces unambiguous.

Generated numeric IDs are currently first-class on the SQL-family runtimes, Prisma, and the in-memory driver. MongoDB and Mongoose keep manual numeric IDs only for now.

Field builders

Scalar field types

BuilderOutput typeNotes
id()stringUnique by default and generated as an id
id({ type: "integer" })numberManual numeric primary key support
id({ type: "integer", generated: "increment" })numberAuto-generated numeric primary key support
string()stringPlain string scalar
boolean()booleanBoolean scalar
datetime()DateRepresents a JavaScript Date in runtime typing

Field methods

MethodPurpose
.unique()Marks the field unique
.nullable()Makes the runtime output T | null
.default(value)Stores a literal default
.defaultNow()Marks the field as generated from now
.references("model.field")Declares a foreign-key style reference
.map("column_name")Maps a field name to a different physical column name
.describe("...")Adds documentation metadata for the manifest/docs layer

Model-level constraints

Use model constraints when uniqueness or indexing belongs to a combination of fields instead of a single field.

account: model({
  table: "accounts",
  fields: {
    id: id(),
    userId: string().references("user.id"),
    provider: string(),
    accountId: string(),
  },
  constraints: {
    unique: [["provider", "accountId"]],
    indexes: [["userId", "provider"]],
  },
});

Constraint keys

KeyPurpose
uniqueDeclares one or more unique field sets
indexesDeclares one or more non-unique indexes

Why this matters

Example:

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

Relation helpers

belongsTo

Use this when the current model holds the foreign key.

session: model({
  table: "sessions",
  fields: {
    id: id(),
    userId: string().references("user.id"),
  },
  relations: {
    user: belongsTo("user", { foreignKey: "userId" }),
  },
});

hasOne

Use this when the target model stores a unique foreign key back to the current model.

user: model({
  table: "users",
  fields: { id: id() },
  relations: {
    profile: hasOne("profile", { foreignKey: "userId" }),
  },
});

hasMany

Use this when the target model stores a non-unique foreign key back to the current model.

user: model({
  table: "users",
  fields: { id: id() },
  relations: {
    sessions: hasMany("session", { foreignKey: "userId" }),
  },
});

manyToMany

Use this when you have a join model and want higher-level relation metadata.

organizations: manyToMany("organization", {
  through: "membership",
  from: "userId",
  to: "organizationId",
});

In plain English:

Mapped names and why they matter

This field:

email: string().unique().map("email_address");

means:

What generators use vs. what runtime uses

Today, it is important to separate two layers of value:

That means the current schema relation graph is already useful, but not every generator consumes every relation helper yet.

Manifest layer

Internally, the schema is normalized into a manifest that contains:

That is the handoff point used by the code generators.

Design guidance