Skip to content

Schema DSL

The s.* namespace is nookdb’s schema DSL. Every type, validator, and index in your app starts here.

Shipped builders

import { s } from 'nookdb';
const schema = {
users: s.collection({
id: s.id(), // UUID v7, monotonic
email: s.string().email(), // RFC 5322 validation
name: s.string().min(1).max(100),
age: s.number().int().min(0).optional(), // optional → undefined permitted
role: s.enum(['admin', 'user']).default('user'),
tags: s.array(s.string()),
createdAt: s.date().default(() => new Date()),
}),
};

Available builders

  • s.id() — UUID v7 string. Auto-generated on insert if you don’t supply one.
  • s.string() — UTF-8 string. Chain .min(n), .max(n), .email(), .regex(/…/), .nullable(), .optional(), .default(value | () => value).
  • s.number() — JS number. Chain .int(), .min(n), .max(n), .nullable(), .optional(), .default(...).
  • s.boolean() — Chain .nullable(), .optional(), .default(true | false).
  • s.date() — JS Date. Chain .nullable(), .optional(), .default(...).
  • s.enum([...] as const) — string literal union. The literal types flow through to $type, not just string. Chain .default(...).
  • s.array(itemBuilder) — homogeneous array of any primitive builder (string / number / boolean / date / enum / nested array).
  • s.collection({...}) — wraps a field record into a collection. Chain .index('field'), .index(['a', 'b']), .uniqueIndex('field').

Indexes

const schema = {
posts: s
.collection({ /* fields */ })
.index('authorId') // single-field
.index(['authorId', 'publishedAt']) // composite (prefix-usable)
.uniqueIndex('slug'), // unique + index
};

Indexes are declared on the schema — there is no runtime createIndex. Adding or removing an index triggers a schema-version bump that the migration ledger tracks.

Type inference

type User = typeof schema.users.$type;
// { id: string; email: string; name: string; age?: number | undefined; role: 'admin' | 'user'; ... }

Use $type in app code to keep handwritten types and runtime validation in lockstep. The Rust core re-validates every write against the same descriptor; if the TS type says role: 'admin' | 'user' then Rust will reject role: 'mod' even when TS lies.

Not yet shipped

These appear in early PRDs and design discussions but are not callable in v0.x — listed here so you don’t reach for them and find a TypeError:

  • s.object({...}) — nested struct fields. Currently s.collection is the only structured-record builder; nesting is post-1.0 work (# v2 in src/schema/s.ts).
  • s.ref(() => schema.users) — typed foreign-key references. The same query-time joining can be done by storing the parent id as s.string() and querying both collections; the typed-ref ergonomics are post-1.0.
  • { sparse: true } index option — null-skipping indexes. Currently all indexes include null values; declare the field .nullable() and filter == null rows out of your queries.

When these land they’ll be in the changelog. The shipped surface above is stable across v0.x.