@farming-labs/orm

Fields

Field builders define the scalar shape of each model.

Scalar builders

id()

id: id();

For numeric primary keys, pass an explicit type:

id: id({ type: "integer" });

If you want the runtime or database to generate integer IDs for you, opt into increment semantics:

id: id({ type: "integer", generated: "increment" });

string()

email: string();

boolean()

emailVerified: boolean();

datetime()

createdAt: datetime();

integer()

loginCount: integer();

enumeration()

status: enumeration(["draft", "published"]);

bigint()

quota: bigint();

decimal()

balance: decimal();

json()

metadata: json<{
  plan: string;
  scopes: string[];
}>();

Field modifiers

.unique()

email: string().unique();

Marks the field as unique in the schema and generated outputs.

Use this for single-field uniqueness. For compound uniqueness such as provider + accountId, use model-level constraints.unique on model(...) instead.

.nullable()

bio: string().nullable();

Changes the runtime output type from string to string | null.

.default(...)

role: string().default("member");

Stores a literal default value.

This works well for string, boolean, integer, bigint, decimal, and enum fields today. JSON defaults can still be applied by runtime drivers, but generated schema support is more limited and should be treated as backend-specific for now.

.defaultNow()

createdAt: datetime().defaultNow();

Marks the field as generated from the current time.

.references("model.field")

userId: string().references("user.id");

Adds foreign-key-style metadata that generators can translate.

.map("column_name")

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

Keeps the logical API field name as email while generating the physical column name email_address.

.describe("...")

email: string().describe("Canonical login email");

Adds descriptive metadata to the manifest/docs layer.

Example model

user: model({
  table: "users",
  fields: {
    id: id(),
    email: string().unique().map("email_address"),
    name: string(),
    emailVerified: boolean().default(false),
    loginCount: integer().default(0).map("login_count"),
    tier: enumeration(["free", "pro", "enterprise"]).default("free"),
    quota: bigint().default(0n).map("quota_bigint"),
    balance: decimal().default("0.00"),
    createdAt: datetime().defaultNow(),
    bio: string().nullable(),
  },
});

Numeric IDs use the same DSL:

auditEvent: model({
  table: "audit_events",
  fields: {
    id: id({ type: "integer" }),
    email: string().unique(),
  },
});

Generated numeric IDs use the same builder:

auditEvent: model({
  table: "audit_events",
  fields: {
    id: id({ type: "integer", generated: "increment" }),
    email: string().unique(),
  },
});

What generators care about

Field data is especially important because it drives:

The current generators and live runtimes support:

For SQLite-backed bigint tests, the local matrix enables the underlying node:sqlite big-int read mode so values do not get truncated back into JavaScript number.

Mapping example

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

Logical side:

Physical side:

This is a big part of why the schema DSL is useful for reusable packages: the library can keep a stable logical field name even when the physical database surface needs a different name.