brick.ts

Resources and CRUD

Expose database tables via CRUD resources with field-level access, filtering, and security policies.

llms.txt

Resources link Drizzle database tables to services, while the @brickkit/crud plugin provides type-safe, field-level data operations.

Minimal Example

Attach a declarative CRUD resource to a table with field policies and public access:

src/services/pages.ts
import {
  defineDatabase,
  defineService,
  sqliteTable,
  text,
} from "@brickkit/core";
import { crud } from "@brickkit/crud";

export const pages = sqliteTable("pages", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
  title: text("title").notNull(),
  content: text("content"),
  internalNotes: text("internal_notes"),
});

const database = defineDatabase({ tables: { pages } });
export const pagesService = defineService("pages", { database });

// Register resource and attach the crud() plugin
export const page = pagesService
  .resource({ name: "page", table: pages, id: pages.id })
  .use(
    crud({
      access: "public",
      fields: {
        id: { read: true, filter: ["eq", "in"] },
        title: {
          read: true,
          create: true,
          update: true,
          filter: ["eq", "contains"],
          sort: true,
        },
        content: { read: true, create: true, update: true },
        // internalNotes is omitted: hidden from reads, writes, and filters
      },
    }),
  );

Schema

Brick does not generate owner or timestamp columns. Declare them in your table and populate them with Drizzle defaults or CRUD hooks when needed.

These columns match the Drizzle tables in the example. Defaults marked as application defaults run in Drizzle, rather than in SQL.

Table Name: pages

FieldTypeDescription
idtextRow identifier. PK Default: crypto.randomUUID() (application)
titletextDisplay title.
contenttextPage content. Optional
internal_notestextInternal notes, hidden by the CRUD field policy. Optional

CRUD is Deny-by-Default: access is a required option. access: "public" allows every call. access: { authorize, scope } calls your authorize on every operation and adds your scope condition to queries. An access object without authorize rejects every call with FORBIDDEN (HTTP 403).

Public Action Names

When crud() installs onto pagesService.resource({ name: "page", ... }), actions are registered on the service under dot-separated names prefixed by the resource name. Routes are mounted under ${prefix}/${resource}, not under the service name:

ActionRouteInput
page.listPOST /api/page/query{ select?, where?, orderBy?, limit?, offset?, includeTotal? }
page.getGET /api/page/:id{ id, select? }
page.findOnePOST /api/page/find-oneSame as list; returns the first row or null
page.createPOST /api/pageFields with create: true
page.updatePATCH /api/page/:id{ id, data } with fields that have update: true
page.deleteDELETE /api/page/:id{ id }; returns { success: true }
page.countPOST /api/page/count{ where? }; returns { count }
page.existsPOST /api/page/exists{ where? }; returns { exists }

The action names appear in architecture introspection and client contracts; OpenAPI summaries use service.action (for example, pages.page.list). .use(crud(...)) also returns the resource with these actions attached, so you can call page.list({ input: {} }) in-process. Use the routes option to change a route's path or scope, or set a route to false to keep the action but not mount it over HTTP.

Field Policies

Each column requires explicit permission toggles in the fields object:

src/services/users.ts
crud({
  access: "public",
  fields: {
    id: { read: true, filter: ["eq", "in"] },
    email: {
      read: true,
      create: true,
      filter: ["eq"],
      scrub: ({ value, ctx }) => {
        // Redact email for unauthorized callers
        return ctx.user?.role === "admin" ? value : "[REDACTED]";
      },
    },
    passwordHash: {
      create: true, // Write-only column
      read: false,
    },
  },
});

Field Policy Configuration Table

OptionTypeDescription
readbooleanAllows column to be returned in list, findOne, get, create, and update responses, and selected via select.
createbooleanAccepts column in create input payload.
updatebooleanAccepts column in update input payload.
filterFilterOperator[]Enables filtering on this column. Allowed operators: eq, ne, in, notIn, gt, gte, lt, lte, between, contains, startsWith, endsWith, isNull, isNotNull. contains/startsWith/endsWith require a string column.
sortbooleanEnables ascending/descending ordering on this column.
schemaTSchemaCustom TypeBox schema to override automatic Drizzle column schema inference.
scrub({ value, ctx }) => unknownTransforms a readable field's value before it's returned. Scrubbed fields are typed unknown in the output schema.

Every key in fields must be a column of the table, or crud() throws at setup. The primary key can't have update: true.

Operations Selection and Limits

You can turn off individual operations (or all of them with operations: false) and set pagination limits. defaultLimit defaults to 20 and maxLimit to 100; a limit above maxLimit fails validation:

src/services/readonly.ts
crud({
  access: "public",
  operations: {
    delete: false, // Disables page.delete endpoint entirely
    update: false, // Disables page.update endpoint entirely
  },
  defaultLimit: 25, // Default page size for list queries
  maxLimit: 100, // Maximum allowed limit
  fields: {
    id: { read: true },
    title: { read: true },
  },
});

Performance & Prepared Statement Caching

When the database client supports prepare() (Bun SQLite), CRUD reuses prepared statements:

  • Statements are cached per query shape (operation, filter structure, selected fields, ordering), up to 128 per database, with filter values bound as placeholders.
  • Caching is skipped when a call has a non-empty scope condition, and for any table with a column that uses $defaultFn or $onUpdateFn (such as the $defaultFn UUID in the example above).
  • For resources with no scope function and no such columns, brick() prepares the default list and get statements when it builds the app.